diff --git a/internal/backend/remote-state/s3/backend.go b/internal/backend/remote-state/s3/backend.go index 2474f19df1..173273b6b7 100644 --- a/internal/backend/remote-state/s3/backend.go +++ b/internal/backend/remote-state/s3/backend.go @@ -304,19 +304,9 @@ func (b *Backend) ConfigSchema() *configschema.Block { Description: "The maximum number of times an AWS API request is retried on retryable failure.", }, - "assume_role": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: assumeRoleFullSchema().SchemaAttributes(), - }, - }, + "assume_role": assumeRoleSchema.SchemaAttribute(), - "assume_role_with_web_identity": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: assumeRoleWithWebIdentityFullSchema().SchemaAttributes(), - }, - }, + "assume_role_with_web_identity": assumeRoleWithWebIdentitySchema.SchemaAttribute(), "use_legacy_workflow": { Type: cty.Bool, @@ -354,6 +344,249 @@ func (b *Backend) ConfigSchema() *configschema.Block { } } +var assumeRoleSchema = singleNestedAttribute{ + Attributes: map[string]schemaAttribute{ + "role_arn": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Required: true, + Description: "The role to be assumed.", + }, + validateString{ + Validators: []stringValidator{ + validateARN( + validateIAMRoleARN, + ), + }, + }, + }, + + "duration": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", + }, + validateString{ + Validators: []stringValidator{ + validateDuration( + validateDurationBetween(15*time.Minute, 12*time.Hour), + ), + }, + }, + }, + + "external_id": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The external ID to use when assuming the role", + }, + validateString{ + Validators: []stringValidator{ + validateStringLenBetween(2, 1224), + validateStringMatches( + regexp.MustCompile(`^[\w+=,.@:\/\-]*$`), + `Value can only contain letters, numbers, or the following characters: =,.@/-`, + ), + }, + }, + }, + + "policy": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + }, + validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateIAMPolicyDocument, + }, + }, + }, + + "policy_arns": setAttribute{ + configschema.Attribute{ + Type: cty.Set(cty.String), + Optional: true, + Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", + }, + validateSet{ + Validators: []setValidator{ + validateSetStringElements( + validateARN( + validateIAMPolicyARN, + ), + ), + }, + }, + }, + + "session_name": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The session name to use when assuming the role.", + }, + validateString{ + Validators: []stringValidator{ + validateStringLenBetween(2, 64), + validateStringMatches( + regexp.MustCompile(`^[\w+=,.@\-]*$`), + `Value can only contain letters, numbers, or the following characters: =,.@-`, + ), + }, + }, + }, + + // NOT SUPPORTED by `aws-sdk-go-base/v1` + // "source_identity": stringAttribute{ + // configschema.Attribute{ + // Type: cty.String, + // Optional: true, + // Description: "Source identity specified by the principal assuming the role.", + // ValidateFunc: validAssumeRoleSourceIdentity, + // }, + // }, + + "tags": mapAttribute{ + configschema.Attribute{ + Type: cty.Map(cty.String), + Optional: true, + Description: "Assume role session tags.", + }, + validateMap{}, + }, + + "transitive_tag_keys": setAttribute{ + configschema.Attribute{ + Type: cty.Set(cty.String), + Optional: true, + Description: "Assume role session tag keys to pass to any subsequent sessions.", + }, + validateSet{}, + }, + }, +} + +var assumeRoleWithWebIdentitySchema = singleNestedAttribute{ + Attributes: map[string]schemaAttribute{ + "role_arn": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Required: true, + Description: "The role to be assumed.", + }, + validateString{ + Validators: []stringValidator{ + validateARN( + validateIAMRoleARN, + ), + }, + }, + }, + + "duration": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", + }, + validateString{ + Validators: []stringValidator{ + validateDuration( + validateDurationBetween(15*time.Minute, 12*time.Hour), + ), + }, + }, + }, + + "policy": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + }, + validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateIAMPolicyDocument, + }, + }, + }, + + "policy_arns": setAttribute{ + configschema.Attribute{ + Type: cty.Set(cty.String), + Optional: true, + Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", + }, + validateSet{ + Validators: []setValidator{ + validateSetStringElements( + validateARN( + validateIAMPolicyARN, + ), + ), + }, + }, + }, + + "session_name": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The session name to use when assuming the role.", + }, + validateString{ + Validators: []stringValidator{ + validateStringLenBetween(2, 64), + validateStringMatches( + regexp.MustCompile(`^[\w+=,.@\-]*$`), + `Value can only contain letters, numbers, or the following characters: =,.@-`, + ), + }, + }, + }, + + "web_identity_token": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "Value of a web identity token from an OpenID Connect (OIDC) or OAuth provider.", + }, + validateString{ + Validators: []stringValidator{ + validateStringLenBetween(4, 20000), + }, + }, + }, + + "web_identity_token_file": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider.", + }, + validateString{ + Validators: []stringValidator{ + validateStringLenBetween(4, 20000), + }, + }, + }, + }, + validateObject: validateObject{ + Validators: []objectValidator{ + validateExactlyOneOfAttributes( + cty.GetAttrPath("web_identity_token"), + cty.GetAttrPath("web_identity_token_file"), + ), + }, + }, +} + // PrepareConfig checks the validity of the values in the given // configuration, and inserts any missing defaults, assuming that its // structure has already been validated per the schema returned by @@ -440,7 +673,7 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) } if val := obj.GetAttr("assume_role"); !val.IsNull() { - diags = diags.Append(prepareAssumeRoleConfig(val, cty.GetAttrPath("assume_role"))) + diags = diags.Append(validateNestedAttribute(assumeRoleSchema, val, cty.GetAttrPath("assume_role"))) if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 { diags = diags.Append(tfdiags.WholeContainingBody( @@ -461,7 +694,7 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) } if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { - diags = diags.Append(prepareAssumeRoleWithWebIdentityConfig(val, cty.GetAttrPath("assume_role_with_web_identity"))) + diags = diags.Append(validateNestedAttribute(assumeRoleWithWebIdentitySchema, val, cty.GetAttrPath("assume_role_with_web_identity"))) } validateAttributesConflict( @@ -1196,45 +1429,22 @@ The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS) while "AWS_SSE_CUSTOMER_KEY" is used for encryption with customer-managed keys (SSE-C). Please choose one or the other.` -func prepareAssumeRoleConfig(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics { +// TODO: make diags a parameter +func validateNestedAttribute(objSchema schemaAttribute, obj cty.Value, objPath cty.Path) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if obj.IsNull() { return diags } - for name, attrSchema := range assumeRoleFullSchema() { - attrPath := objPath.GetAttr(name) - attrVal := obj.GetAttr(name) - - if a, e := attrVal.Type(), attrSchema.SchemaAttribute().Type; a != e { - diags = diags.Append(attributeErrDiag( - "Internal Error", - fmt.Sprintf(`Expected type to be %s, got: %s`, e.FriendlyName(), a.FriendlyName()), - attrPath, - )) - continue - } - - if attrVal.IsNull() { - if attrSchema.SchemaAttribute().Required { - diags = diags.Append(requiredAttributeErrDiag(attrPath)) - } - continue - } - - validator := attrSchema.Validator() - validator.ValidateAttr(attrVal, attrPath, &diags) - } - - return diags -} -func prepareAssumeRoleWithWebIdentityConfig(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - if obj.IsNull() { + na, ok := objSchema.(singleNestedAttribute) + if !ok { return diags } - for name, attrSchema := range assumeRoleWithWebIdentityFullSchema() { + validator := objSchema.Validator() + validator.ValidateAttr(obj, objPath, &diags) + + for name, attrSchema := range na.Attributes { attrPath := objPath.GetAttr(name) attrVal := obj.GetAttr(name) @@ -1258,11 +1468,6 @@ func prepareAssumeRoleWithWebIdentityConfig(obj cty.Value, objPath cty.Path) tfd validator.ValidateAttr(attrVal, attrPath, &diags) } - validateExactlyOneOfAttributes( - cty.GetAttrPath("web_identity_token"), - cty.GetAttrPath("web_identity_token_file"), - )(obj, objPath, &diags) - return diags } @@ -1343,11 +1548,23 @@ func (v validateSet) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdia } } +type validateObject struct { + Validators []objectValidator +} + +func (v validateObject) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) { + for _, validator := range v.Validators { + validator(val, attrPath, diags) + } +} + type schemaAttribute interface { SchemaAttribute() *configschema.Attribute Validator() validateSchema } +var _ schemaAttribute = stringAttribute{} + type stringAttribute struct { configschema.Attribute validateString @@ -1361,6 +1578,8 @@ func (a stringAttribute) Validator() validateSchema { return a.validateString } +var _ schemaAttribute = setAttribute{} + type setAttribute struct { configschema.Attribute validateSet @@ -1374,6 +1593,8 @@ func (a setAttribute) Validator() validateSchema { return a.validateSet } +var _ schemaAttribute = mapAttribute{} + type mapAttribute struct { configschema.Attribute validateMap @@ -1397,239 +1618,24 @@ func (s objectSchema) SchemaAttributes() map[string]*configschema.Attribute { return m } -func assumeRoleFullSchema() objectSchema { - return map[string]schemaAttribute{ - "role_arn": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Required: true, - Description: "The role to be assumed.", - }, - validateString{ - Validators: []stringValidator{ - validateARN( - validateIAMRoleARN, - ), - }, - }, - }, +var _ schemaAttribute = singleNestedAttribute{} - "duration": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", - }, - validateString{ - Validators: []stringValidator{ - validateDuration( - validateDurationBetween(15*time.Minute, 12*time.Hour), - ), - }, - }, - }, - - "external_id": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "The external ID to use when assuming the role", - }, - validateString{ - Validators: []stringValidator{ - validateStringLenBetween(2, 1224), - validateStringMatches( - regexp.MustCompile(`^[\w+=,.@:\/\-]*$`), - `Value can only contain letters, numbers, or the following characters: =,.@/-`, - ), - }, - }, - }, - - "policy": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", - }, - validateString{ - Validators: []stringValidator{ - validateStringNotEmpty, - validateIAMPolicyDocument, - }, - }, - }, - - "policy_arns": setAttribute{ - configschema.Attribute{ - Type: cty.Set(cty.String), - Optional: true, - Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", - }, - validateSet{ - Validators: []setValidator{ - validateSetStringElements( - validateARN( - validateIAMPolicyARN, - ), - ), - }, - }, - }, - - "session_name": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "The session name to use when assuming the role.", - }, - validateString{ - Validators: []stringValidator{ - validateStringLenBetween(2, 64), - validateStringMatches( - regexp.MustCompile(`^[\w+=,.@\-]*$`), - `Value can only contain letters, numbers, or the following characters: =,.@-`, - ), - }, - }, - }, - - // NOT SUPPORTED by `aws-sdk-go-base/v1` - // "source_identity": stringAttribute{ - // configschema.Attribute{ - // Type: cty.String, - // Optional: true, - // Description: "Source identity specified by the principal assuming the role.", - // ValidateFunc: validAssumeRoleSourceIdentity, - // }, - // }, +type singleNestedAttribute struct { + Attributes objectSchema + validateObject +} - "tags": mapAttribute{ - configschema.Attribute{ - Type: cty.Map(cty.String), - Optional: true, - Description: "Assume role session tags.", - }, - validateMap{}, - }, - - "transitive_tag_keys": setAttribute{ - configschema.Attribute{ - Type: cty.Set(cty.String), - Optional: true, - Description: "Assume role session tag keys to pass to any subsequent sessions.", - }, - validateSet{}, +func (a singleNestedAttribute) SchemaAttribute() *configschema.Attribute { + return &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: a.Attributes.SchemaAttributes(), }, } } -func assumeRoleWithWebIdentityFullSchema() objectSchema { - return map[string]schemaAttribute{ - "role_arn": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Required: true, - Description: "The role to be assumed.", - }, - validateString{ - Validators: []stringValidator{ - validateARN( - validateIAMRoleARN, - ), - }, - }, - }, - - "duration": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", - }, - validateString{ - Validators: []stringValidator{ - validateDuration( - validateDurationBetween(15*time.Minute, 12*time.Hour), - ), - }, - }, - }, - - "policy": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", - }, - validateString{ - Validators: []stringValidator{ - validateStringNotEmpty, - validateIAMPolicyDocument, - }, - }, - }, - - "policy_arns": setAttribute{ - configschema.Attribute{ - Type: cty.Set(cty.String), - Optional: true, - Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", - }, - validateSet{ - Validators: []setValidator{ - validateSetStringElements( - validateARN( - validateIAMPolicyARN, - ), - ), - }, - }, - }, - - "session_name": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "The session name to use when assuming the role.", - }, - validateString{ - Validators: []stringValidator{ - validateStringLenBetween(2, 64), - validateStringMatches( - regexp.MustCompile(`^[\w+=,.@\-]*$`), - `Value can only contain letters, numbers, or the following characters: =,.@-`, - ), - }, - }, - }, - - "web_identity_token": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "Value of a web identity token from an OpenID Connect (OIDC) or OAuth provider.", - }, - validateString{ - Validators: []stringValidator{ - validateStringLenBetween(4, 20000), - }, - }, - }, - - "web_identity_token_file": stringAttribute{ - configschema.Attribute{ - Type: cty.String, - Optional: true, - Description: "File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider.", - }, - validateString{ - Validators: []stringValidator{ - validateStringLenBetween(4, 20000), - }, - }, - }, - } +func (a singleNestedAttribute) Validator() validateSchema { + return a.validateObject } func deprecatedAttrDiag(attr, replacement cty.Path) tfdiags.Diagnostic { diff --git a/internal/backend/remote-state/s3/backend_complete_test.go b/internal/backend/remote-state/s3/backend_complete_test.go index f3a6c0495c..a8d1ec342c 100644 --- a/internal/backend/remote-state/s3/backend_complete_test.go +++ b/internal/backend/remote-state/s3/backend_complete_test.go @@ -1872,14 +1872,14 @@ web_identity_token_file = no-such-file ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ attributeErrDiag( "Missing Required Value", - `The attribute "assume_role_with_web_identity.role_arn" is required by the backend.`+"\n\n"+ - "Refer to the backend documentation for additional information which attributes are required.", - cty.GetAttrPath("assume_role_with_web_identity").GetAttr("role_arn"), + `Exactly one of web_identity_token, web_identity_token_file must be set.`, + cty.GetAttrPath("assume_role_with_web_identity"), ), attributeErrDiag( "Missing Required Value", - `Exactly one of web_identity_token, web_identity_token_file must be set.`, - cty.GetAttrPath("assume_role_with_web_identity"), + `The attribute "assume_role_with_web_identity.role_arn" is required by the backend.`+"\n\n"+ + "Refer to the backend documentation for additional information which attributes are required.", + cty.GetAttrPath("assume_role_with_web_identity").GetAttr("role_arn"), ), }), }, diff --git a/internal/backend/remote-state/s3/backend_test.go b/internal/backend/remote-state/s3/backend_test.go index 228ba3e407..2a58eac8c7 100644 --- a/internal/backend/remote-state/s3/backend_test.go +++ b/internal/backend/remote-state/s3/backend_test.go @@ -1971,7 +1971,7 @@ func TestAssumeRole_PrepareConfigValidation(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - schema := assumeRoleFullSchema() + schema := assumeRoleSchema.Attributes vals := make(map[string]cty.Value, len(schema)) for name, attrSchema := range schema { if val, ok := tc.config[name]; ok { @@ -1982,7 +1982,7 @@ func TestAssumeRole_PrepareConfigValidation(t *testing.T) { } config := cty.ObjectVal(vals) - diags := prepareAssumeRoleConfig(config, path) + diags := validateNestedAttribute(assumeRoleSchema, config, path) if diff := cmp.Diff(diags, tc.expectedDiags, cmp.Comparer(diagnosticComparer)); diff != "" { t.Errorf("unexpected diagnostics difference: %s", diff)