lang/funcs: Make nonsensitive more permissive

Calling the nonsensitive function with values which are not sensitive
will result in an error. This restriction was added with the goal of
preventing confusingly redundant use of this function.

Unfortunately, this breaks when using nonsensitive to reveal the value of
sensitive resource attributes. This is because the validate walk does
not (and cannot) mark attributes as sensitive based on the schema,
because the resource value itself is unknown.

This commit therefore alters this restriction such that it permits
nonsensitive unknown values, and adds a test case to cover this specific
scenario.
This commit is contained in:
Alisdair McDiarmid 2021-04-09 14:33:49 -04:00
parent 1212bbec9f
commit c1f7193454
3 changed files with 71 additions and 6 deletions

View File

@ -48,7 +48,7 @@ var NonsensitiveFunc = function.New(&function.Spec{
return args[0].Type(), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
if !args[0].HasMark("sensitive") {
if args[0].IsKnown() && !args[0].HasMark("sensitive") {
return cty.DynamicVal, function.NewArgErrorf(0, "the given value is not sensitive, so this call is redundant")
}
v, marks := args[0].Unmark()

View File

@ -133,17 +133,20 @@ func TestNonsensitive(t *testing.T) {
cty.NumberIntVal(1),
`the given value is not sensitive, so this call is redundant`,
},
{
cty.DynamicVal,
`the given value is not sensitive, so this call is redundant`,
},
{
cty.NullVal(cty.String),
`the given value is not sensitive, so this call is redundant`,
},
// Unknown values may become sensitive once they are known, so we
// permit them to be marked nonsensitive.
{
cty.DynamicVal,
``,
},
{
cty.UnknownVal(cty.String),
`the given value is not sensitive, so this call is redundant`,
``,
},
}

View File

@ -377,3 +377,65 @@ resource "test_object" "a" {
}
}
}
func TestContext2Plan_unmarkingSensitiveAttributeForOutput(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_resource" "foo" {
}
output "result" {
value = nonsensitive(test_resource.foo.sensitive_attr)
}
`,
})
p := new(MockProvider)
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_resource": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"sensitive_attr": {
Type: cty.String,
Computed: true,
Sensitive: true,
},
},
},
},
})
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: cty.UnknownVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"sensitive_attr": cty.String,
})),
}
}
state := states.NewState()
ctx := testContext2(t, &ContextOpts{
Config: m,
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
State: state,
})
plan, diags := ctx.Plan()
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
for _, res := range plan.Changes.Resources {
if res.Action != plans.Create {
t.Fatalf("expected create, got: %q %s", res.Addr, res.Action)
}
}
}