diff --git a/internal/backend/remote-state/s3/backend.go b/internal/backend/remote-state/s3/backend.go index 042f5f6c14..15e25cd2e8 100644 --- a/internal/backend/remote-state/s3/backend.go +++ b/internal/backend/remote-state/s3/backend.go @@ -94,33 +94,9 @@ func (b *Backend) ConfigSchema() *configschema.Block { Description: "A custom endpoint for the S3 API", Deprecated: true, }, - "endpoints": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "dynamodb": { - Type: cty.String, - Optional: true, - Description: "A custom endpoint for the DynamoDB API", - }, - "iam": { - Type: cty.String, - Optional: true, - Description: "A custom endpoint for the IAM API", - }, - "s3": { - Type: cty.String, - Optional: true, - Description: "A custom endpoint for the S3 API", - }, - "sts": { - Type: cty.String, - Optional: true, - Description: "A custom endpoint for the STS API", - }, - }, - }, - }, + + "endpoints": endpointsSchema.SchemaAttribute(), + "forbidden_account_ids": { Type: cty.Set(cty.String), Optional: true, @@ -304,19 +280,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,132 +320,431 @@ func (b *Backend) ConfigSchema() *configschema.Block { } } -// 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 -// ConfigSchema. -func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - if obj.IsNull() { - return obj, diags - } - - var attrPath cty.Path - - attrPath = cty.GetAttrPath("bucket") - if val := obj.GetAttr("bucket"); val.IsNull() { - diags = diags.Append(requiredAttributeErrDiag(attrPath)) - } else { - bucketValidators := validateString{ - Validators: []stringValidator{ - validateStringNotEmpty, +var assumeRoleSchema = singleNestedAttribute{ + Attributes: map[string]schemaAttribute{ + "role_arn": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Required: true, + Description: "The role to be assumed.", }, - } - bucketValidators.ValidateAttr(val, attrPath, &diags) - } - - attrPath = cty.GetAttrPath("key") - if val := obj.GetAttr("key"); val.IsNull() { - diags = diags.Append(requiredAttributeErrDiag(attrPath)) - } else { - keyValidators := validateString{ - Validators: []stringValidator{ - validateStringNotEmpty, - validateStringS3Path, - validateStringDoesNotContain("//"), + validateString{ + Validators: []stringValidator{ + validateARN( + validateIAMRoleARN, + ), + }, }, - } - keyValidators.ValidateAttr(val, attrPath, &diags) - } + }, - // Not updating region handling, because validation will be handled by `aws-sdk-go-base` once it is updated - if val := obj.GetAttr("region"); val.IsNull() || val.AsString() == "" { - if os.Getenv("AWS_REGION") == "" && os.Getenv("AWS_DEFAULT_REGION") == "" { - diags = diags.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Missing region value", - `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`, - cty.GetAttrPath("region"), - )) - } - } + "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), + ), + }, + }, + }, - validateAttributesConflict( - cty.GetAttrPath("kms_key_id"), - cty.GetAttrPath("sse_customer_key"), - )(obj, cty.Path{}, &diags) + "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: =,.@/-`, + ), + }, + }, + }, - attrPath = cty.GetAttrPath("kms_key_id") - if val := obj.GetAttr("kms_key_id"); !val.IsNull() { - kmsKeyIDValidators := validateString{ - Validators: []stringValidator{ - validateStringKMSKey, + "policy": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", }, - } - kmsKeyIDValidators.ValidateAttr(val, attrPath, &diags) - } + validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateIAMPolicyDocument, + }, + }, + }, - attrPath = cty.GetAttrPath("workspace_key_prefix") - if val := obj.GetAttr("workspace_key_prefix"); !val.IsNull() { - keyPrefixValidators := validateString{ - Validators: []stringValidator{ - validateStringS3Path, + "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.", }, - } - keyPrefixValidators.ValidateAttr(val, attrPath, &diags) - } + validateSet{ + Validators: []setValidator{ + validateSetStringElements( + validateARN( + validateIAMPolicyARN, + ), + ), + }, + }, + }, - var assumeRoleDeprecatedFields = map[string]string{ - "role_arn": "assume_role.role_arn", - "session_name": "assume_role.session_name", - "external_id": "assume_role.external_id", - "assume_role_duration_seconds": "assume_role.duration", - "assume_role_policy": "assume_role.policy", - "assume_role_policy_arns": "assume_role.policy_arns", - "assume_role_tags": "assume_role.tags", - "assume_role_transitive_tag_keys": "assume_role.transitive_tag_keys", - } + "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: =,.@-`, + ), + }, + }, + }, - if val := obj.GetAttr("assume_role"); !val.IsNull() { - diags = diags.Append(prepareAssumeRoleConfig(val, cty.GetAttrPath("assume_role"))) + // 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, + // }, + // }, - if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 { - diags = diags.Append(tfdiags.WholeContainingBody( - tfdiags.Error, - "Conflicting Parameters", - `The following deprecated parameters conflict with the parameter "assume_role". Replace them as follows:`+"\n"+ - formatDeprecations(defined), - )) - } - } else { - if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 { - diags = diags.Append(wholeBodyWarningDiag( - "Deprecated Parameters", - `The following parameters have been deprecated. Replace them as follows:`+"\n"+ - formatDeprecations(defined), - )) - } - } + "tags": mapAttribute{ + configschema.Attribute{ + Type: cty.Map(cty.String), + Optional: true, + Description: "Assume role session tags.", + }, + validateMap{}, + }, - if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { - diags = diags.Append(prepareAssumeRoleWithWebIdentityConfig(val, cty.GetAttrPath("assume_role_with_web_identity"))) - } + "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{}, + }, + }, +} - validateAttributesConflict( - cty.GetAttrPath("shared_credentials_file"), - cty.GetAttrPath("shared_credentials_files"), - )(obj, cty.Path{}, &diags) +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, + ), + }, + }, + }, - attrPath = cty.GetAttrPath("shared_credentials_file") - if val := obj.GetAttr("shared_credentials_file"); !val.IsNull() { - diags = diags.Append(deprecatedAttrDiag(attrPath, cty.GetAttrPath("shared_credentials_files"))) - } + "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), + ), + }, + }, + }, - endpointFields := map[string]string{ - "dynamodb_endpoint": "dynamodb", - "iam_endpoint": "iam", - "endpoint": "s3", - "sts_endpoint": "sts", + "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"), + ), + }, + }, +} + +var endpointsSchema = singleNestedAttribute{ + Attributes: map[string]schemaAttribute{ + "dynamodb": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the DynamoDB API", + }, + validateString{ + Validators: []stringValidator{ + validateStringURL, + }, + }, + }, + + "iam": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the IAM API", + }, + validateString{ + Validators: []stringValidator{ + validateStringURL, + }, + }, + }, + + "s3": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the S3 API", + }, + validateString{ + Validators: []stringValidator{ + validateStringURL, + }, + }, + }, + + "sts": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the STS API", + }, + validateString{ + Validators: []stringValidator{ + validateStringURL, + }, + }, + }, + }, +} + +// 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 +// ConfigSchema. +func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + if obj.IsNull() { + return obj, diags + } + + var attrPath cty.Path + + attrPath = cty.GetAttrPath("bucket") + if val := obj.GetAttr("bucket"); val.IsNull() { + diags = diags.Append(requiredAttributeErrDiag(attrPath)) + } else { + bucketValidators := validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + }, + } + bucketValidators.ValidateAttr(val, attrPath, &diags) + } + + attrPath = cty.GetAttrPath("key") + if val := obj.GetAttr("key"); val.IsNull() { + diags = diags.Append(requiredAttributeErrDiag(attrPath)) + } else { + keyValidators := validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateStringS3Path, + validateStringDoesNotContain("//"), + }, + } + keyValidators.ValidateAttr(val, attrPath, &diags) + } + + // Not updating region handling, because validation will be handled by `aws-sdk-go-base` once it is updated + if val := obj.GetAttr("region"); val.IsNull() || val.AsString() == "" { + if os.Getenv("AWS_REGION") == "" && os.Getenv("AWS_DEFAULT_REGION") == "" { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Missing region value", + `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`, + cty.GetAttrPath("region"), + )) + } + } + + validateAttributesConflict( + cty.GetAttrPath("kms_key_id"), + cty.GetAttrPath("sse_customer_key"), + )(obj, cty.Path{}, &diags) + + attrPath = cty.GetAttrPath("kms_key_id") + if val := obj.GetAttr("kms_key_id"); !val.IsNull() { + kmsKeyIDValidators := validateString{ + Validators: []stringValidator{ + validateStringKMSKey, + }, + } + kmsKeyIDValidators.ValidateAttr(val, attrPath, &diags) + } + + attrPath = cty.GetAttrPath("workspace_key_prefix") + if val := obj.GetAttr("workspace_key_prefix"); !val.IsNull() { + keyPrefixValidators := validateString{ + Validators: []stringValidator{ + validateStringS3Path, + }, + } + keyPrefixValidators.ValidateAttr(val, attrPath, &diags) + } + + var assumeRoleDeprecatedFields = map[string]string{ + "role_arn": "assume_role.role_arn", + "session_name": "assume_role.session_name", + "external_id": "assume_role.external_id", + "assume_role_duration_seconds": "assume_role.duration", + "assume_role_policy": "assume_role.policy", + "assume_role_policy_arns": "assume_role.policy_arns", + "assume_role_tags": "assume_role.tags", + "assume_role_transitive_tag_keys": "assume_role.transitive_tag_keys", + } + + if val := obj.GetAttr("assume_role"); !val.IsNull() { + validateNestedAttribute(assumeRoleSchema, val, cty.GetAttrPath("assume_role"), &diags) + + if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 { + diags = diags.Append(tfdiags.WholeContainingBody( + tfdiags.Error, + "Conflicting Parameters", + `The following deprecated parameters conflict with the parameter "assume_role". Replace them as follows:`+"\n"+ + formatDeprecations(defined), + )) + } + } else { + if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 { + diags = diags.Append(wholeBodyWarningDiag( + "Deprecated Parameters", + `The following parameters have been deprecated. Replace them as follows:`+"\n"+ + formatDeprecations(defined), + )) + } + } + + if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { + validateNestedAttribute(assumeRoleWithWebIdentitySchema, val, cty.GetAttrPath("assume_role_with_web_identity"), &diags) + } + + validateAttributesConflict( + cty.GetAttrPath("shared_credentials_file"), + cty.GetAttrPath("shared_credentials_files"), + )(obj, cty.Path{}, &diags) + + attrPath = cty.GetAttrPath("shared_credentials_file") + if val := obj.GetAttr("shared_credentials_file"); !val.IsNull() { + diags = diags.Append(deprecatedAttrDiag(attrPath, cty.GetAttrPath("shared_credentials_files"))) + } + + endpointFields := map[string]string{ + "dynamodb_endpoint": "dynamodb", + "iam_endpoint": "iam", + "endpoint": "s3", + "sts_endpoint": "sts", } endpoints := make(map[string]string) if val := obj.GetAttr("endpoints"); !val.IsNull() { @@ -504,20 +769,15 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) } } + if val := obj.GetAttr("endpoints"); !val.IsNull() { + validateNestedAttribute(endpointsSchema, val, cty.GetAttrPath("endpoints"), &diags) + } + endpointValidators := validateString{ Validators: []stringValidator{ validateStringURL, }, } - if val := obj.GetAttr("endpoints"); !val.IsNull() { - attrPath := cty.GetAttrPath("endpoints") - for _, k := range []string{"dynamodb", "iam", "s3", "sts"} { - if v := val.GetAttr(k); !v.IsNull() { - attrPath := attrPath.GetAttr(k) - endpointValidators.ValidateAttr(v, attrPath, &diags) - } - } - } for _, k := range maps.Keys(endpointFields) { if val := obj.GetAttr(k); !val.IsNull() { attrPath := cty.GetAttrPath(k) @@ -1197,50 +1457,25 @@ 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 { - var diags tfdiags.Diagnostics +func validateNestedAttribute(objSchema schemaAttribute, obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) { if obj.IsNull() { - return diags + return } - 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) + na, ok := objSchema.(singleNestedAttribute) + if !ok { + return } - return diags -} -func prepareAssumeRoleWithWebIdentityConfig(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - if obj.IsNull() { - return diags - } + validator := objSchema.Validator() + validator.ValidateAttr(obj, objPath, diags) - for name, attrSchema := range assumeRoleWithWebIdentityFullSchema() { + for name, attrSchema := range na.Attributes { attrPath := objPath.GetAttr(name) attrVal := obj.GetAttr(name) if a, e := attrVal.Type(), attrSchema.SchemaAttribute().Type; a != e { - diags = diags.Append(attributeErrDiag( + *diags = diags.Append(attributeErrDiag( "Internal Error", fmt.Sprintf(`Expected type to be %s, got: %s`, e.FriendlyName(), a.FriendlyName()), attrPath, @@ -1250,21 +1485,14 @@ func prepareAssumeRoleWithWebIdentityConfig(obj cty.Value, objPath cty.Path) tfd if attrVal.IsNull() { if attrSchema.SchemaAttribute().Required { - diags = diags.Append(requiredAttributeErrDiag(attrPath)) + *diags = diags.Append(requiredAttributeErrDiag(attrPath)) } continue } validator := attrSchema.Validator() - validator.ValidateAttr(attrVal, attrPath, &diags) + validator.ValidateAttr(attrVal, attrPath, diags) } - - validateExactlyOneOfAttributes( - cty.GetAttrPath("web_identity_token"), - cty.GetAttrPath("web_identity_token_file"), - )(obj, objPath, &diags) - - return diags } func requiredAttributeErrDiag(path cty.Path) tfdiags.Diagnostic { @@ -1344,11 +1572,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 @@ -1362,6 +1602,8 @@ func (a stringAttribute) Validator() validateSchema { return a.validateString } +var _ schemaAttribute = setAttribute{} + type setAttribute struct { configschema.Attribute validateSet @@ -1375,6 +1617,8 @@ func (a setAttribute) Validator() validateSchema { return a.validateSet } +var _ schemaAttribute = mapAttribute{} + type mapAttribute struct { configschema.Attribute validateMap @@ -1398,239 +1642,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, - ), - }, - }, - }, - - "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: =,.@-`, - ), - }, - }, - }, +var _ schemaAttribute = singleNestedAttribute{} - // 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{}, - }, +type singleNestedAttribute struct { + Attributes objectSchema + validateObject +} - "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 85fa6e0e7c..a8d1ec342c 100644 --- a/internal/backend/remote-state/s3/backend_complete_test.go +++ b/internal/backend/remote-state/s3/backend_complete_test.go @@ -1869,14 +1869,28 @@ web_identity_token_file = no-such-file config: map[string]any{ "assume_role_with_web_identity": map[string]any{}, }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ + attributeErrDiag( + "Missing Required Value", + `Exactly one of web_identity_token, web_identity_token_file must be set.`, + cty.GetAttrPath("assume_role_with_web_identity"), + ), 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"), ), + }), + }, + + "invalid no token": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + }, + }, + ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ attributeErrDiag( "Missing Required Value", `Exactly one of web_identity_token, web_identity_token_file must be set.`, @@ -1885,17 +1899,19 @@ web_identity_token_file = no-such-file }), }, - "invalid no token": { + "invalid token config conflict": { config: map[string]any{ "assume_role_with_web_identity": map[string]any{ - "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + "web_identity_token": servicemocks.MockWebIdentityToken, }, }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + SetConfig: true, ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ attributeErrDiag( - "Missing Required Value", - `Exactly one of web_identity_token, web_identity_token_file must be set.`, + "Invalid Attribute Combination", + `Only one of web_identity_token, web_identity_token_file can be set.`, cty.GetAttrPath("assume_role_with_web_identity"), ), }), diff --git a/internal/backend/remote-state/s3/backend_test.go b/internal/backend/remote-state/s3/backend_test.go index a0bd7ee94b..31536bbce5 100644 --- a/internal/backend/remote-state/s3/backend_test.go +++ b/internal/backend/remote-state/s3/backend_test.go @@ -2025,7 +2025,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 { @@ -2036,7 +2036,8 @@ func TestAssumeRole_PrepareConfigValidation(t *testing.T) { } config := cty.ObjectVal(vals) - diags := prepareAssumeRoleConfig(config, path) + var diags tfdiags.Diagnostics + validateNestedAttribute(assumeRoleSchema, config, path, &diags) if diff := cmp.Diff(diags, tc.expectedDiags, cmp.Comparer(diagnosticComparer)); diff != "" { t.Errorf("unexpected diagnostics difference: %s", diff)