diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bdf73e256..5f30899904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,8 @@ S3 BACKEND: * The S3 backend was upgraded to use the V2 of the AWS SDK for Go ([#691](https://github.com/opentofu/opentofu/issues/691)) * Adds support for `shared_config_files` and `shared_credentials_files` arguments and deprecates the `shared_credentials_file` argument. ([#690](https://github.com/opentofu/opentofu/issues/690)) - +* Arguments associated with assuming an IAM role were moved into a nested block - `assume_role`. + This deprecates the arguments `role_arn`, `session_name`, `external_id`, `assume_role_duration_seconds`, `assume_role_policy`, `assume_role_policy_arns`, `assume_role_tags`, and `assume_role_transitive_tag_keys`. ([#747](https://github.com/opentofu/opentofu/issues/747)) ## Previous Releases diff --git a/internal/backend/remote-state/s3/backend.go b/internal/backend/remote-state/s3/backend.go index bf6beff7fe..8521636ab4 100644 --- a/internal/backend/remote-state/s3/backend.go +++ b/internal/backend/remote-state/s3/backend.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "fmt" "os" + "sort" "strings" "time" @@ -165,46 +166,54 @@ func (b *Backend) ConfigSchema() *configschema.Block { Type: cty.String, Optional: true, Description: "The role to be assumed", + Deprecated: true, }, "session_name": { Type: cty.String, Optional: true, Description: "The session name to use when assuming the role.", + Deprecated: true, }, "external_id": { Type: cty.String, Optional: true, Description: "The external ID to use when assuming the role", + Deprecated: true, }, "assume_role_duration_seconds": { Type: cty.Number, Optional: true, Description: "Seconds to restrict the assume role session duration.", + Deprecated: true, }, "assume_role_policy": { Type: cty.String, Optional: true, Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + Deprecated: true, }, "assume_role_policy_arns": { 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.", + Deprecated: true, }, "assume_role_tags": { Type: cty.Map(cty.String), Optional: true, Description: "Assume role session tags.", + Deprecated: true, }, "assume_role_transitive_tag_keys": { Type: cty.Set(cty.String), Optional: true, Description: "Assume role session tag keys to pass to any subsequent sessions.", + Deprecated: true, }, "workspace_key_prefix": { @@ -229,6 +238,65 @@ func (b *Backend) ConfigSchema() *configschema.Block { Optional: true, Description: "Use the legacy authentication workflow, preferring environment variables over backend configuration.", }, + "assume_role": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "role_arn": { + Type: cty.String, + Required: true, + Description: "The role to be assumed.", + }, + "duration": { + Type: cty.String, + Optional: true, + Description: "Seconds to restrict the assume role session duration.", + }, + "external_id": { + Type: cty.String, + Optional: true, + Description: "The external ID to use when assuming the role", + }, + "policy": { + Type: cty.String, + Optional: true, + Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + }, + "policy_arns": { + 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.", + }, + "session_name": { + Type: cty.String, + Optional: true, + Description: "The session name to use when assuming the role.", + }, + "tags": { + Type: cty.Map(cty.String), + Optional: true, + Description: "Assume role session tags.", + }, + "transitive_tag_keys": { + Type: cty.Set(cty.String), + Optional: true, + Description: "Assume role session tag keys to pass to any subsequent sessions.", + }, + // + // NOT SUPPORTED by `aws-sdk-go-base/v1` + // Cannot be added yet. + // + // "source_identity": stringAttribute{ + // configschema.Attribute{ + // Type: cty.String, + // Optional: true, + // Description: "Source identity specified by the principal assuming the role.", + // ValidateFunc: validAssumeRoleSourceIdentity, + // }, + // }, + }, + }, + }, }, } } @@ -332,6 +400,39 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) attrPath)) } + 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() { + diags = diags.Append(validateNestedAssumeRole(val, cty.Path{cty.GetAttrStep{Name: "assume_role"}})) + + 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"+ + formatDeprecated(defined), + )) + } + } else { + if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 { + diags = diags.Append(tfdiags.WholeContainingBody( + tfdiags.Warning, + "Deprecated Parameters", + `The following parameters have been deprecated. Replace them as follows:`+"\n"+ + formatDeprecated(defined), + )) + } + } + return obj, diags } @@ -455,7 +556,9 @@ func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { } } - if value := obj.GetAttr("role_arn"); !value.IsNull() { + if value := obj.GetAttr("assume_role"); !value.IsNull() { + cfg.AssumeRole = configureNestedAssumeRole(obj) + } else if value := obj.GetAttr("role_arn"); !value.IsNull() { cfg.AssumeRole = configureAssumeRole(obj) } @@ -509,6 +612,46 @@ func getS3Config(obj cty.Value) func(options *s3.Options) { } } +func configureNestedAssumeRole(obj cty.Value) *awsbase.AssumeRole { + assumeRole := awsbase.AssumeRole{} + + obj = obj.GetAttr("assume_role") + if val, ok := stringAttrOk(obj, "role_arn"); ok { + assumeRole.RoleARN = val + } + if val, ok := stringAttrOk(obj, "duration"); ok { + dur, err := time.ParseDuration(val) + if err != nil { + // This should never happen because the schema should have + // already validated the duration. + panic(fmt.Sprintf("invalid duration %q: %s", val, err)) + } + + assumeRole.Duration = dur + } + if val, ok := stringAttrOk(obj, "external_id"); ok { + assumeRole.ExternalID = val + } + + if val, ok := stringAttrOk(obj, "policy"); ok { + assumeRole.Policy = strings.TrimSpace(val) + } + if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok { + assumeRole.PolicyARNs = val + } + if val, ok := stringAttrOk(obj, "session_name"); ok { + assumeRole.SessionName = val + } + if val, ok := stringMapAttrOk(obj, "tags"); ok { + assumeRole.Tags = val + } + if val, ok := stringSliceAttrOk(obj, "transitive_tag_keys"); ok { + assumeRole.TransitiveTagKeys = val + } + + return &assumeRole +} + func configureAssumeRole(obj cty.Value) *awsbase.AssumeRole { assumeRole := awsbase.AssumeRole{} @@ -518,36 +661,14 @@ func configureAssumeRole(obj cty.Value) *awsbase.AssumeRole { assumeRole.Policy = stringAttr(obj, "assume_role_policy") assumeRole.SessionName = stringAttr(obj, "session_name") - if value := obj.GetAttr("assume_role_policy_arns"); !value.IsNull() { - value.ForEachElement(func(key, val cty.Value) (stop bool) { - v, ok := stringValueOk(val) - if ok { - assumeRole.PolicyARNs = append(assumeRole.PolicyARNs, v) - } - return - }) + if val, ok := stringSliceAttrOk(obj, "assume_role_policy_arns"); ok { + assumeRole.PolicyARNs = val } - - if tagMap := obj.GetAttr("assume_role_tags"); !tagMap.IsNull() { - assumeRole.Tags = make(map[string]string, tagMap.LengthInt()) - tagMap.ForEachElement(func(key, val cty.Value) (stop bool) { - k := stringValue(key) - v, ok := stringValueOk(val) - if ok { - assumeRole.Tags[k] = v - } - return - }) + if val, ok := stringMapAttrOk(obj, "assume_role_tags"); ok { + assumeRole.Tags = val } - - if transitiveTagKeySet := obj.GetAttr("assume_role_transitive_tag_keys"); !transitiveTagKeySet.IsNull() { - transitiveTagKeySet.ForEachElement(func(key, val cty.Value) (stop bool) { - v, ok := stringValueOk(val) - if ok { - assumeRole.TransitiveTagKeys = append(assumeRole.TransitiveTagKeys, v) - } - return - }) + if val, ok := stringSliceAttrOk(obj, "assume_role_transitive_tag_keys"); ok { + assumeRole.TransitiveTagKeys = val } return &assumeRole @@ -670,6 +791,19 @@ func intAttrDefault(obj cty.Value, name string, def int) int { } } +func stringMapValueOk(val cty.Value) (map[string]string, bool) { + var m map[string]string + err := gocty.FromCtyValue(val, &m) + if err != nil { + return nil, false + } + return m, true +} + +func stringMapAttrOk(obj cty.Value, name string) (map[string]string, bool) { + return stringMapValueOk(obj.GetAttr(name)) +} + func pathString(path cty.Path) string { var buf strings.Builder for i, step := range path { @@ -707,6 +841,35 @@ func pathString(path cty.Path) string { return buf.String() } +func findDeprecatedFields(obj cty.Value, attrs map[string]string) map[string]string { + defined := make(map[string]string) + for attr, v := range attrs { + if val := obj.GetAttr(attr); !val.IsNull() { + defined[attr] = v + } + } + return defined +} + +func formatDeprecated(attrs map[string]string) string { + var maxLen int + var buf strings.Builder + + names := make([]string, 0, len(attrs)) + for deprecated, replacement := range attrs { + names = append(names, deprecated) + if l := len(deprecated); l > maxLen { + maxLen = l + } + + fmt.Fprintf(&buf, " * %-[1]*[2]s -> %s\n", maxLen, deprecated, replacement) + } + + sort.Strings(names) + + return buf.String() +} + const encryptionKeyConflictError = `Only one of "kms_key_id" and "sse_customer_key" can be set. The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS) diff --git a/internal/backend/remote-state/s3/backend_complete_test.go b/internal/backend/remote-state/s3/backend_complete_test.go index 82d49f5987..beef2598da 100644 --- a/internal/backend/remote-state/s3/backend_complete_test.go +++ b/internal/backend/remote-state/s3/backend_complete_test.go @@ -16,7 +16,14 @@ import ( "github.com/opentofu/opentofu/internal/tfdiags" ) -type DiagsValidator func(*testing.T, tfdiags.Diagnostics) +const mockStsAssumeRolePolicy = `{ + "Version": "2012-10-17", + "Statement": { + "Effect": "Allow", + "Action": "*", + "Resource": "*" + } + }` func ExpectNoDiags(t *testing.T, diags tfdiags.Diagnostics) { expectDiagsCount(t, diags, 0) @@ -28,7 +35,7 @@ func expectDiagsCount(t *testing.T, diags tfdiags.Diagnostics, c int) { } } -func ExpectDiagsEqual(expected tfdiags.Diagnostics) DiagsValidator { +func ExpectDiagsEqual(expected tfdiags.Diagnostics) diagsValidator { return func(t *testing.T, diags tfdiags.Diagnostics) { if diff := cmp.Diff(diags, expected, cmp.Comparer(diagnosticComparer)); diff != "" { t.Fatalf("unexpected diagnostics difference: %s", diff) @@ -36,8 +43,10 @@ func ExpectDiagsEqual(expected tfdiags.Diagnostics) DiagsValidator { } } -// ExpectDiagMatching returns a validator expeceting a single Diagnostic with fields matching the expectation -func ExpectDiagMatching(severity tfdiags.Severity, summary matcher, detail matcher) DiagsValidator { +type diagsValidator func(*testing.T, tfdiags.Diagnostics) + +// ExpectDiagsMatching returns a validator expeceting a single Diagnostic with fields matching the expectation +func ExpectDiagsMatching(severity tfdiags.Severity, summary matcher, detail matcher) diagsValidator { return func(t *testing.T, diags tfdiags.Diagnostics) { for _, d := range diags { if !summary.Match(d.Description().Summary) || !detail.Match(d.Description().Detail) { @@ -56,6 +65,38 @@ func ExpectDiagMatching(severity tfdiags.Severity, summary matcher, detail match } } +type diagValidator func(*testing.T, tfdiags.Diagnostic) + +func ExpectDiagMatching(severity tfdiags.Severity, summary matcher, detail matcher) diagValidator { + return func(t *testing.T, d tfdiags.Diagnostic) { + if !summary.Match(d.Description().Summary) || !detail.Match(d.Description().Detail) { + t.Fatalf("expected Diagnostic matching %#v, got %#v", + tfdiags.Sourceless( + severity, + summary.String(), + detail.String(), + ), + d, + ) + } + } +} + +func ExpectMultipleDiags(validators ...diagValidator) diagsValidator { + return func(t *testing.T, diags tfdiags.Diagnostics) { + count := len(validators) + if diagCount := len(diags); diagCount < count { + count = diagCount + } + + for i := 0; i < count; i++ { + validators[i](t, diags[i]) + } + + expectDiagsCount(t, diags, len(validators)) + } +} + type matcher interface { fmt.Stringer Match(string) bool @@ -89,6 +130,16 @@ func (m regexpMatcher) String() string { return m.re.String() } +type noopMatcher struct{} + +func (m noopMatcher) Match(s string) bool { + return true +} + +func (m noopMatcher) String() string { + return "" +} + func TestBackendConfig_Authentication(t *testing.T) { testCases := map[string]struct { config map[string]any @@ -101,11 +152,11 @@ func TestBackendConfig_Authentication(t *testing.T) { MockStsEndpoints []*servicemocks.MockEndpoint SharedConfigurationFile string SharedCredentialsFile string - ValidateDiags DiagsValidator + ValidateDiags diagsValidator }{ "empty config": { config: map[string]any{}, - ValidateDiags: ExpectDiagMatching( + ValidateDiags: ExpectDiagsMatching( tfdiags.Error, equalsMatcher("No valid credential sources found"), newRegexpMatcher("^Please see.+"), @@ -124,131 +175,6 @@ func TestBackendConfig_Authentication(t *testing.T) { ValidateDiags: ExpectNoDiags, }, - "config AccessKey config AssumeRoleARN access key": { - config: map[string]any{ - "access_key": servicemocks.MockStaticAccessKey, - "secret_key": servicemocks.MockStaticSecretKey, - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - - "config AssumeRoleDuration": { - config: map[string]any{ - "access_key": servicemocks.MockStaticAccessKey, - "secret_key": servicemocks.MockStaticSecretKey, - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - "assume_role_duration_seconds": 3600, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"DurationSeconds": "3600"}), - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - - "config AssumeRoleExternalID": { - config: map[string]any{ - "access_key": servicemocks.MockStaticAccessKey, - "secret_key": servicemocks.MockStaticSecretKey, - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - "external_id": servicemocks.MockStsAssumeRoleExternalId, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"ExternalId": servicemocks.MockStsAssumeRoleExternalId}), - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - - "config AssumeRolePolicy": { - config: map[string]any{ - "access_key": servicemocks.MockStaticAccessKey, - "secret_key": servicemocks.MockStaticSecretKey, - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - "assume_role_policy": servicemocks.MockStsAssumeRolePolicy, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Policy": servicemocks.MockStsAssumeRolePolicy}), - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - - "config AssumeRolePolicyARNs": { - config: map[string]any{ - "access_key": servicemocks.MockStaticAccessKey, - "secret_key": servicemocks.MockStaticSecretKey, - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - "assume_role_policy_arns": []any{servicemocks.MockStsAssumeRolePolicyArn}, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"PolicyArns.member.1.arn": servicemocks.MockStsAssumeRolePolicyArn}), - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - - "config AssumeRoleTags": { - config: map[string]any{ - "access_key": servicemocks.MockStaticAccessKey, - "secret_key": servicemocks.MockStaticSecretKey, - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - "assume_role_tags": map[string]any{ - servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, - }, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Tags.member.1.Key": servicemocks.MockStsAssumeRoleTagKey, "Tags.member.1.Value": servicemocks.MockStsAssumeRoleTagValue}), - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - - "config AssumeRoleTransitiveTagKeys": { - config: map[string]any{ - "access_key": servicemocks.MockStaticAccessKey, - "secret_key": servicemocks.MockStaticSecretKey, - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - "assume_role_tags": map[string]any{ - servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, - }, - "assume_role_transitive_tag_keys": []any{servicemocks.MockStsAssumeRoleTagKey}, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Tags.member.1.Key": servicemocks.MockStsAssumeRoleTagKey, "Tags.member.1.Value": servicemocks.MockStsAssumeRoleTagValue, "TransitiveTagKeys.member.1": servicemocks.MockStsAssumeRoleTagKey}), - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - - // NOT SUPPORTED: AssumeRoleSourceIdentity - // "config AssumeRoleSourceIdentity": { - // config: map[string]any{ - // "access_key": servicemocks.MockStaticAccessKey, - // "secret_key": servicemocks.MockStaticSecretKey, - // "role_arn": servicemocks.MockStsAssumeRoleArn, - // "session_name": servicemocks.MockStsAssumeRoleSessionName, - // "assume_role_source_identity": servicemocks.MockStsAssumeRoleSourceIdentity, - // }, - // ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - // MockStsEndpoints: []*servicemocks.MockEndpoint{ - // servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"SourceIdentity": servicemocks.MockStsAssumeRoleSourceIdentity}), - // servicemocks.MockStsGetCallerIdentityValidEndpoint, - // }, - // }, - "config Profile shared credentials profile aws_access_key_id": { config: map[string]any{ "profile": "SharedCredentialsProfile", @@ -324,45 +250,6 @@ aws_secret_access_key = ProfileSharedCredentialsSecretKey ValidateDiags: ExpectNoDiags, }, - "config Profile shared configuration credential_source Ec2InstanceMetadata": { - config: map[string]any{ - "profile": "SharedConfigurationProfile", - }, - EnableEc2MetadataServer: true, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - SharedConfigurationFile: fmt.Sprintf(` -[profile SharedConfigurationProfile] -credential_source = Ec2InstanceMetadata -role_arn = %[1]s -role_session_name = %[2]s -`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), - }, - - "config Profile shared configuration source_profile": { - config: map[string]any{ - "profile": "SharedConfigurationProfile", - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - SharedConfigurationFile: fmt.Sprintf(` -[profile SharedConfigurationProfile] -role_arn = %[1]s -role_session_name = %[2]s -source_profile = SharedConfigurationSourceProfile - -[profile SharedConfigurationSourceProfile] -aws_access_key_id = SharedConfigurationSourceAccessKey -aws_secret_access_key = SharedConfigurationSourceSecretKey -`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), - }, - "environment AWS_ACCESS_KEY_ID": { config: map[string]any{}, EnvironmentVariables: map[string]string{ @@ -376,22 +263,6 @@ aws_secret_access_key = SharedConfigurationSourceSecretKey ValidateDiags: ExpectNoDiags, }, - "environment AWS_ACCESS_KEY_ID config AssumeRoleARN access key": { - config: map[string]any{ - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - }, - EnvironmentVariables: map[string]string{ - "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, - "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - "environment AWS_PROFILE shared credentials profile aws_access_key_id": { config: map[string]any{}, EnvironmentVariables: map[string]string{ @@ -417,47 +288,6 @@ aws_secret_access_key = ProfileSharedCredentialsSecretKey ValidateDiags: ExpectNoDiags, }, - "environment AWS_PROFILE shared configuration credential_source Ec2InstanceMetadata": { - config: map[string]any{}, - EnableEc2MetadataServer: true, - EnvironmentVariables: map[string]string{ - "AWS_PROFILE": "SharedConfigurationProfile", - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - SharedConfigurationFile: fmt.Sprintf(` -[profile SharedConfigurationProfile] -credential_source = Ec2InstanceMetadata -role_arn = %[1]s -role_session_name = %[2]s -`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), - }, - - "environment AWS_PROFILE shared configuration source_profile": { - config: map[string]any{}, - EnvironmentVariables: map[string]string{ - "AWS_PROFILE": "SharedConfigurationProfile", - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - SharedConfigurationFile: fmt.Sprintf(` -[profile SharedConfigurationProfile] -role_arn = %[1]s -role_session_name = %[2]s -source_profile = SharedConfigurationSourceProfile - -[profile SharedConfigurationSourceProfile] -aws_access_key_id = SharedConfigurationSourceAccessKey -aws_secret_access_key = SharedConfigurationSourceSecretKey -`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), - }, - "environment AWS_SESSION_TOKEN": { config: map[string]any{}, EnvironmentVariables: map[string]string{ @@ -488,23 +318,6 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey `, }, - "shared credentials default aws_access_key_id config AssumeRoleARN access key": { - config: map[string]any{ - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - }, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - SharedCredentialsFile: ` -[default] -aws_access_key_id = DefaultSharedCredentialsAccessKey -aws_secret_access_key = DefaultSharedCredentialsSecretKey -`, - }, - "web identity token access key": { config: map[string]any{}, EnableWebIdentityEnvVars: true, @@ -525,19 +338,6 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey ValidateDiags: ExpectNoDiags, }, - "EC2 metadata access key config AssumeRoleARN access key": { - config: map[string]any{ - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - }, - EnableEc2MetadataServer: true, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - "ECS credentials access key": { config: map[string]any{}, EnableEcsCredentialsServer: true, @@ -547,19 +347,6 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey }, }, - "ECS credentials access key config AssumeRoleARN access key": { - config: map[string]any{ - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - }, - EnableEcsCredentialsServer: true, - ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleValidEndpoint, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - }, - "AssumeWebIdentity envvar AssumeRoleARN access key": { config: map[string]any{ "role_arn": servicemocks.MockStsAssumeRoleArn, @@ -572,23 +359,13 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey servicemocks.MockStsAssumeRoleValidEndpoint, servicemocks.MockStsGetCallerIdentityValidEndpoint, }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), }, - // NOT SUPPORTED: AssumeWebIdentity config - // "AssumeWebIdentity config AssumeRoleARN access key": { - // config: map[string]any{ - // "role_arn": servicemocks.MockStsAssumeRoleArn, - // "session_name": servicemocks.MockStsAssumeRoleSessionName, - // }, - // EnableWebIdentityConfig: true, - // ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, - // MockStsEndpoints: []*servicemocks.MockEndpoint{ - // servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, - // servicemocks.MockStsAssumeRoleValidEndpoint, - // servicemocks.MockStsGetCallerIdentityValidEndpoint, - // }, - // }, - "config AccessKey over environment AWS_ACCESS_KEY_ID": { config: map[string]any{ "access_key": servicemocks.MockStaticAccessKey, @@ -747,24 +524,6 @@ region = us-east-1 `, }, - "assume role error": { - config: map[string]any{ - "access_key": servicemocks.MockStaticAccessKey, - "secret_key": servicemocks.MockStaticSecretKey, - "role_arn": servicemocks.MockStsAssumeRoleArn, - "session_name": servicemocks.MockStsAssumeRoleSessionName, - }, - MockStsEndpoints: []*servicemocks.MockEndpoint{ - servicemocks.MockStsAssumeRoleInvalidEndpointInvalidClientTokenId, - servicemocks.MockStsGetCallerIdentityValidEndpoint, - }, - ValidateDiags: ExpectDiagMatching( - tfdiags.Error, - equalsMatcher("Cannot assume IAM Role"), - newRegexpMatcher(`IAM Role \(.+\) cannot be assumed.`), - ), - }, - "skip EC2 Metadata API check": { config: map[string]any{ "skip_metadata_api_check": true, @@ -774,7 +533,7 @@ region = us-east-1 MockStsEndpoints: []*servicemocks.MockEndpoint{ servicemocks.MockStsGetCallerIdentityValidEndpoint, }, - ValidateDiags: ExpectDiagMatching( + ValidateDiags: ExpectDiagsMatching( tfdiags.Error, equalsMatcher("No valid credential sources found"), newRegexpMatcher("^Please see.+"), @@ -791,7 +550,7 @@ region = us-east-1 aws_access_key_id = DefaultSharedCredentialsAccessKey aws_secret_access_key = DefaultSharedCredentialsSecretKey `, - ValidateDiags: ExpectDiagMatching( + ValidateDiags: ExpectDiagsMatching( tfdiags.Error, equalsMatcher("failed to get shared config profile, no-such-profile"), equalsMatcher(""), @@ -807,7 +566,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey aws_access_key_id = DefaultSharedCredentialsAccessKey aws_secret_access_key = DefaultSharedCredentialsSecretKey `, - ValidateDiags: ExpectDiagMatching( + ValidateDiags: ExpectDiagsMatching( tfdiags.Error, equalsMatcher("failed to get shared config profile, no-such-profile"), equalsMatcher(""), @@ -849,7 +608,7 @@ aws_secret_access_key = ProfileSharedCredentialsSecretKey aws_access_key_id = DefaultSharedCredentialsAccessKey aws_secret_access_key = DefaultSharedCredentialsSecretKey `, - ValidateDiags: ExpectDiagMatching( + ValidateDiags: ExpectDiagsMatching( tfdiags.Error, equalsMatcher("failed to get shared config profile, no-such-profile"), equalsMatcher(""), @@ -983,6 +742,851 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey }) } } +func TestBackendConfig_Authentication_AssumeRoleInline(t *testing.T) { + testCases := map[string]struct { + config map[string]any + EnableEc2MetadataServer bool + EnableEcsCredentialsServer bool + EnvironmentVariables map[string]string + ExpectedCredentialsValue aws.Credentials + MockStsEndpoints []*servicemocks.MockEndpoint + SharedConfigurationFile string + SharedCredentialsFile string + ValidateDiags diagsValidator + }{ + "from config access_key": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "from environment AWS_ACCESS_KEY_ID": { + config: map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "from config Profile with Ec2InstanceMetadata source": { + config: map[string]any{ + "profile": "SharedConfigurationProfile", + }, + EnableEc2MetadataServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +credential_source = Ec2InstanceMetadata +role_arn = %[1]s +role_session_name = %[2]s +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + "from environment AWS_PROFILE with Ec2InstanceMetadata source": { + config: map[string]any{}, + EnableEc2MetadataServer: true, + EnvironmentVariables: map[string]string{ + "AWS_PROFILE": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +credential_source = Ec2InstanceMetadata +role_arn = %[1]s +role_session_name = %[2]s +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + "from config Profile with source profile": { + config: map[string]any{ + "profile": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +role_arn = %[1]s +role_session_name = %[2]s +source_profile = SharedConfigurationSourceProfile + +[profile SharedConfigurationSourceProfile] +aws_access_key_id = SharedConfigurationSourceAccessKey +aws_secret_access_key = SharedConfigurationSourceSecretKey +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + "from environment AWS_PROFILE with source profile": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_PROFILE": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +role_arn = %[1]s +role_session_name = %[2]s +source_profile = SharedConfigurationSourceProfile + +[profile SharedConfigurationSourceProfile] +aws_access_key_id = SharedConfigurationSourceAccessKey +aws_secret_access_key = SharedConfigurationSourceSecretKey +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + "from default profile": { + config: map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "from EC2 metadata": { + config: map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + EnableEc2MetadataServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "from ECS credentials": { + config: map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + EnableEcsCredentialsServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "with duration": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "assume_role_duration_seconds": 3600, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"DurationSeconds": "3600"}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "with external ID": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "external_id": servicemocks.MockStsAssumeRoleExternalId, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"ExternalId": servicemocks.MockStsAssumeRoleExternalId}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "with policy": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "assume_role_policy": mockStsAssumeRolePolicy, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Policy": mockStsAssumeRolePolicy}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "with policy ARNs": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "assume_role_policy_arns": []any{servicemocks.MockStsAssumeRolePolicyArn}, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"PolicyArns.member.1.arn": servicemocks.MockStsAssumeRolePolicyArn}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "with tags": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "assume_role_tags": map[string]any{ + servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Tags.member.1.Key": servicemocks.MockStsAssumeRoleTagKey, "Tags.member.1.Value": servicemocks.MockStsAssumeRoleTagValue}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "with transitive tags": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "assume_role_tags": map[string]any{ + servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, + }, + "assume_role_transitive_tag_keys": []any{servicemocks.MockStsAssumeRoleTagKey}, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Tags.member.1.Key": servicemocks.MockStsAssumeRoleTagKey, "Tags.member.1.Value": servicemocks.MockStsAssumeRoleTagValue, "TransitiveTagKeys.member.1": servicemocks.MockStsAssumeRoleTagKey}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + }, + + "error": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleInvalidEndpointInvalidClientTokenId, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectMultipleDiags( + ExpectDiagMatching( + tfdiags.Warning, + equalsMatcher("Deprecated Parameters"), + noopMatcher{}, + ), + ExpectDiagMatching( + tfdiags.Error, + equalsMatcher("Cannot assume IAM Role"), + noopMatcher{}, + ), + ), + }, + } + + for name, tc := range testCases { + tc := tc + + t.Run(name, func(t *testing.T) { + oldEnv := servicemocks.InitSessionTestEnv() + defer servicemocks.PopEnv(oldEnv) + + ctx := context.TODO() + + // Populate required fields + tc.config["region"] = "us-east-1" + tc.config["bucket"] = "bucket" + tc.config["key"] = "key" + + if tc.ValidateDiags == nil { + tc.ValidateDiags = ExpectNoDiags + } + + if tc.EnableEc2MetadataServer { + closeEc2Metadata := servicemocks.AwsMetadataApiMock(append( + servicemocks.Ec2metadata_securityCredentialsEndpoints, + servicemocks.Ec2metadata_instanceIdEndpoint, + servicemocks.Ec2metadata_iamInfoEndpoint, + )) + defer closeEc2Metadata() + } + + if tc.EnableEcsCredentialsServer { + closeEcsCredentials := servicemocks.EcsCredentialsApiMock() + defer closeEcsCredentials() + } + + ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints) + defer ts.Close() + + tc.config["sts_endpoint"] = ts.URL + + if tc.SharedConfigurationFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared configuration file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared configuration file: %s", err) + } + + setSharedConfigFile(file.Name()) + } + + if tc.SharedCredentialsFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-credentials-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared credentials file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedCredentialsFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared credentials file: %s", err) + } + + tc.config["shared_credentials_files"] = []any{file.Name()} + if tc.ExpectedCredentialsValue.Source == "SharedConfigCredentials" { + tc.ExpectedCredentialsValue.Source = fmt.Sprintf("SharedConfigCredentials: %s", file.Name()) + } + } + + for k, v := range tc.EnvironmentVariables { + os.Setenv(k, v) + } + + b, diags := configureBackend(t, tc.config) + + tc.ValidateDiags(t, diags) + + if diags.HasErrors() { + return + } + + credentials, err := b.awsConfig.Credentials.Retrieve(ctx) + if err != nil { + t.Fatalf("Error when requesting credentials: %s", err) + } + + if diff := cmp.Diff(credentials, tc.ExpectedCredentialsValue, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" { + t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff) + } + }) + } +} + +func TestBackendConfig_Authentication_AssumeRoleNested(t *testing.T) { + testCases := map[string]struct { + config map[string]any + EnableEc2MetadataServer bool + EnableEcsCredentialsServer bool + EnvironmentVariables map[string]string + ExpectedCredentialsValue aws.Credentials + MockStsEndpoints []*servicemocks.MockEndpoint + SharedConfigurationFile string + SharedCredentialsFile string + ValidateDiags diagsValidator + }{ + "from config access_key": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "from environment AWS_ACCESS_KEY_ID": { + config: map[string]any{ + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "from config Profile with Ec2InstanceMetadata source": { + config: map[string]any{ + "profile": "SharedConfigurationProfile", + }, + EnableEc2MetadataServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +credential_source = Ec2InstanceMetadata +role_arn = %[1]s +role_session_name = %[2]s +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + "from environment AWS_PROFILE with Ec2InstanceMetadata source": { + config: map[string]any{}, + EnableEc2MetadataServer: true, + EnvironmentVariables: map[string]string{ + "AWS_PROFILE": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +credential_source = Ec2InstanceMetadata +role_arn = %[1]s +role_session_name = %[2]s +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + "from config Profile with source profile": { + config: map[string]any{ + "profile": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +role_arn = %[1]s +role_session_name = %[2]s +source_profile = SharedConfigurationSourceProfile + +[profile SharedConfigurationSourceProfile] +aws_access_key_id = SharedConfigurationSourceAccessKey +aws_secret_access_key = SharedConfigurationSourceSecretKey +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + "from environment AWS_PROFILE with source profile": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_PROFILE": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +role_arn = %[1]s +role_session_name = %[2]s +source_profile = SharedConfigurationSourceProfile + +[profile SharedConfigurationSourceProfile] +aws_access_key_id = SharedConfigurationSourceAccessKey +aws_secret_access_key = SharedConfigurationSourceSecretKey +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + "from default profile": { + config: map[string]any{ + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + }, + + "from EC2 metadata": { + config: map[string]any{ + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + EnableEc2MetadataServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "from ECS credentials": { + config: map[string]any{ + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + EnableEcsCredentialsServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "with duration": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "duration": "1h", + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"DurationSeconds": "3600"}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "with external ID": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "external_id": servicemocks.MockStsAssumeRoleExternalId, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"ExternalId": servicemocks.MockStsAssumeRoleExternalId}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "with policy": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "policy": mockStsAssumeRolePolicy, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Policy": mockStsAssumeRolePolicy}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "with policy ARNs": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "policy_arns": []any{servicemocks.MockStsAssumeRolePolicyArn}, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"PolicyArns.member.1.arn": servicemocks.MockStsAssumeRolePolicyArn}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "with tags": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "tags": map[string]any{ + servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, + }, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Tags.member.1.Key": servicemocks.MockStsAssumeRoleTagKey, "Tags.member.1.Value": servicemocks.MockStsAssumeRoleTagValue}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "with transitive tags": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "tags": map[string]any{ + servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, + }, + "transitive_tag_keys": []any{servicemocks.MockStsAssumeRoleTagKey}, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Tags.member.1.Key": servicemocks.MockStsAssumeRoleTagKey, "Tags.member.1.Value": servicemocks.MockStsAssumeRoleTagValue, "TransitiveTagKeys.member.1": servicemocks.MockStsAssumeRoleTagKey}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "error": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleInvalidEndpointInvalidClientTokenId, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagsMatching( + tfdiags.Error, + equalsMatcher("Cannot assume IAM Role"), + noopMatcher{}, + ), + }, + } + + for name, tc := range testCases { + tc := tc + + t.Run(name, func(t *testing.T) { + oldEnv := servicemocks.InitSessionTestEnv() + defer servicemocks.PopEnv(oldEnv) + + ctx := context.TODO() + + // Populate required fields + tc.config["region"] = "us-east-1" + tc.config["bucket"] = "bucket" + tc.config["key"] = "key" + + if tc.ValidateDiags == nil { + tc.ValidateDiags = ExpectNoDiags + } + + if tc.EnableEc2MetadataServer { + closeEc2Metadata := servicemocks.AwsMetadataApiMock(append( + servicemocks.Ec2metadata_securityCredentialsEndpoints, + servicemocks.Ec2metadata_instanceIdEndpoint, + servicemocks.Ec2metadata_iamInfoEndpoint, + )) + defer closeEc2Metadata() + } + + if tc.EnableEcsCredentialsServer { + closeEcsCredentials := servicemocks.EcsCredentialsApiMock() + defer closeEcsCredentials() + } + + ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints) + defer ts.Close() + + tc.config["sts_endpoint"] = ts.URL + + if tc.SharedConfigurationFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared configuration file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared configuration file: %s", err) + } + + setSharedConfigFile(file.Name()) + } + + if tc.SharedCredentialsFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-credentials-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared credentials file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedCredentialsFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared credentials file: %s", err) + } + + tc.config["shared_credentials_files"] = []any{file.Name()} + if tc.ExpectedCredentialsValue.Source == "SharedConfigCredentials" { + tc.ExpectedCredentialsValue.Source = fmt.Sprintf("SharedConfigCredentials: %s", file.Name()) + } + + tc.config["shared_config_files"] = []any{file.Name()} + } + + for k, v := range tc.EnvironmentVariables { + os.Setenv(k, v) + } + + b, diags := configureBackend(t, tc.config) + + tc.ValidateDiags(t, diags) + + if diags.HasErrors() { + return + } + + credentials, err := b.awsConfig.Credentials.Retrieve(ctx) + if err != nil { + t.Fatalf("Error when requesting credentials: %s", err) + } + + if diff := cmp.Diff(credentials, tc.ExpectedCredentialsValue, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" { + t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff) + } + }) + } +} func TestBackendConfig_Region(t *testing.T) { testCases := map[string]struct { diff --git a/internal/backend/remote-state/s3/validate.go b/internal/backend/remote-state/s3/validate.go index 26d271d198..5e3ee51f80 100644 --- a/internal/backend/remote-state/s3/validate.go +++ b/internal/backend/remote-state/s3/validate.go @@ -4,6 +4,7 @@ import ( "fmt" "regexp" "strings" + "time" "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/opentofu/opentofu/internal/tfdiags" @@ -63,6 +64,96 @@ func validateKMSKeyARN(path cty.Path, s string) (diags tfdiags.Diagnostics) { return diags } +func validateNestedAssumeRole(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if val, ok := stringAttrOk(obj, "role_arn"); !ok || val == "" { + path := objPath.GetAttr("role_arn") + diags = diags.Append(attributeErrDiag( + "Missing Required Value", + fmt.Sprintf("The attribute %q is required by the backend.\n\n", pathString(path))+ + "Refer to the backend documentation for additional information which attributes are required.", + path, + )) + } + + if val, ok := stringAttrOk(obj, "duration"); ok { + path := objPath.GetAttr("duration") + d, err := time.ParseDuration(val) + if err != nil { + diags = diags.Append(attributeErrDiag( + "Invalid Duration", + fmt.Sprintf("The value %q cannot be parsed as a duration: %s", val, err), + path, + )) + } else { + min := 15 * time.Minute + max := 12 * time.Hour + if d < min || d > max { + diags = diags.Append(attributeErrDiag( + "Invalid Duration", + fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, val), + path, + )) + } + } + } + + if val, ok := stringAttrOk(obj, "external_id"); ok { + if len(strings.TrimSpace(val)) == 0 { + diags = diags.Append(attributeErrDiag( + "Invalid Value", + "The value cannot be empty or all whitespace", + objPath.GetAttr("external_id"), + )) + } + } + + if val, ok := stringAttrOk(obj, "policy"); ok { + if len(strings.TrimSpace(val)) == 0 { + diags = diags.Append(attributeErrDiag( + "Invalid Value", + "The value cannot be empty or all whitespace", + objPath.GetAttr("policy"), + )) + } + } + + if val, ok := stringAttrOk(obj, "session_name"); ok { + if len(strings.TrimSpace(val)) == 0 { + diags = diags.Append(attributeErrDiag( + "Invalid Value", + "The value cannot be empty or all whitespace", + objPath.GetAttr("session_name"), + )) + } + } + + if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok { + for _, v := range val { + arn, err := arn.Parse(v) + if err != nil { + diags = diags.Append(attributeErrDiag( + "Invalid ARN", + fmt.Sprintf("The value %q cannot be parsed as an ARN: %s", val, err), + objPath.GetAttr("policy_arns"), + )) + break + } else { + if !strings.HasPrefix(arn.Resource, "policy/") { + diags = diags.Append(attributeErrDiag( + "Invalid IAM Policy ARN", + fmt.Sprintf("Value must be a valid IAM Policy ARN, got %q", val), + objPath.GetAttr("policy_arns"), + )) + } + } + } + } + + return diags +} + func isKeyARN(arn arn.ARN) bool { return keyIdFromARNResource(arn.Resource) != "" || aliasIdFromARNResource(arn.Resource) != "" } diff --git a/internal/backend/remote-state/s3/validate_test.go b/internal/backend/remote-state/s3/validate_test.go index 710f3f6d29..7b74946c07 100644 --- a/internal/backend/remote-state/s3/validate_test.go +++ b/internal/backend/remote-state/s3/validate_test.go @@ -188,3 +188,150 @@ func Test_validateAttributesConflict(t *testing.T) { }) } } + +func Test_validateNestedAssumeRole(t *testing.T) { + tests := []struct { + description string + input cty.Value + expectedDiags []string + }{ + { + description: "Valid Input", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("valid-role-arn"), + "duration": cty.StringVal("30m"), + "external_id": cty.StringVal("valid-external-id"), + "policy": cty.StringVal("valid-policy"), + "session_name": cty.StringVal("valid-session-name"), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), + }), + expectedDiags: nil, + }, + { + description: "Missing Role ARN", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal(""), + "duration": cty.StringVal("30m"), + "external_id": cty.StringVal("valid-external-id"), + "policy": cty.StringVal("valid-policy"), + "session_name": cty.StringVal("valid-session-name"), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), + }), + expectedDiags: []string{ + "The attribute \"assume_role.role_arn\" is required by the backend.\n\nRefer to the backend documentation for additional information which attributes are required.", + }, + }, + { + description: "Invalid Duration", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("valid-role-arn"), + "duration": cty.StringVal("invalid-duration"), + "external_id": cty.StringVal("valid-external-id"), + "policy": cty.StringVal("valid-policy"), + "session_name": cty.StringVal("valid-session-name"), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), + }), + expectedDiags: []string{ + "The value \"invalid-duration\" cannot be parsed as a duration: time: invalid duration \"invalid-duration\"", + }, + }, + { + description: "Invalid Duration Length", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("valid-role-arn"), + "duration": cty.StringVal("44h"), + "external_id": cty.StringVal("valid-external-id"), + "policy": cty.StringVal("valid-policy"), + "session_name": cty.StringVal("valid-session-name"), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), + }), + expectedDiags: []string{ + "Duration must be between 15m0s and 12h0m0s, had 44h", + }, + }, + { + description: "Invalid External ID (Empty)", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("valid-role-arn"), + "duration": cty.StringVal("30m"), + "external_id": cty.StringVal(""), + "policy": cty.StringVal("valid-policy"), + "session_name": cty.StringVal("valid-session-name"), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), + }), + expectedDiags: []string{ + "The value cannot be empty or all whitespace", + }, + }, + { + description: "Invalid Policy (Empty)", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("valid-role-arn"), + "duration": cty.StringVal("30m"), + "external_id": cty.StringVal("valid-external-id"), + "policy": cty.StringVal(""), + "session_name": cty.StringVal("valid-session-name"), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), + }), + expectedDiags: []string{ + "The value cannot be empty or all whitespace", + }, + }, + { + description: "Invalid Session Name (Empty)", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("valid-role-arn"), + "duration": cty.StringVal("30m"), + "external_id": cty.StringVal("valid-external-id"), + "policy": cty.StringVal("valid-policy"), + "session_name": cty.StringVal(""), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), + }), + expectedDiags: []string{ + "The value cannot be empty or all whitespace", + }, + }, + { + description: "Invalid Policy ARN (Invalid ARN Format)", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("valid-role-arn"), + "duration": cty.StringVal("30m"), + "external_id": cty.StringVal("valid-external-id"), + "policy": cty.StringVal("valid-policy"), + "session_name": cty.StringVal("valid-session-name"), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("invalid-arn-format")}), + }), + expectedDiags: []string{ + "The value [\"invalid-arn-format\"] cannot be parsed as an ARN: arn: invalid prefix", + }, + }, + { + description: "Invalid Policy ARN (Not Starting with 'policy/')", + input: cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("valid-role-arn"), + "duration": cty.StringVal("30m"), + "external_id": cty.StringVal("valid-external-id"), + "policy": cty.StringVal("valid-policy"), + "session_name": cty.StringVal("valid-session-name"), + "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:role/invalid-policy-arn")}), + }), + expectedDiags: []string{ + "Value must be a valid IAM Policy ARN, got [\"arn:aws:iam::123456789012:role/invalid-policy-arn\"]", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + diagnostics := validateNestedAssumeRole(test.input, cty.Path{cty.GetAttrStep{Name: "assume_role"}}) + if len(diagnostics) != len(test.expectedDiags) { + t.Errorf("Expected %d diagnostics, but got %d", len(test.expectedDiags), len(diagnostics)) + } + for i, diag := range diagnostics { + if diag.Description().Detail != test.expectedDiags[i] { + t.Errorf("Mismatch in diagnostic %d. Expected: %q, Got: %q", i, test.expectedDiags[i], diag.Description().Detail) + } + } + }) + } +} diff --git a/website/docs/language/settings/backends/s3.mdx b/website/docs/language/settings/backends/s3.mdx index ab413ab1f8..8c38e67119 100644 --- a/website/docs/language/settings/backends/s3.mdx +++ b/website/docs/language/settings/backends/s3.mdx @@ -174,16 +174,55 @@ The following configuration is optional: #### Assume Role Configuration -The following configuration is optional: +Assuming an IAM Role is optional and can be configured in two ways. +The preferred way is to use the argument `assume_role`, as the other, the other method is deprecated. + +The argument `assume_role` contains the following arguments: + +* `role_arn` - (Required) The Amazon Resource Name (ARN) of the IAM Role to be assumed. +* `duration` - (Optional) Specifies the validity period for individual credentials. + These credentials are automatically renewed, with the maximum renewal defined by the AWS account. + The duration should be specified in the format `hms`, with each unit being optional. + For example, an hour and a half can be represented as `1h30m` or simply `90m`. + The duration must be within the range of 15 minutes (15m) to 12 hours (12h). +* `external_id` - (Optional) An external identifier to use when assuming the role. +* `policy` - (Optional) JSON representation of an IAM Policy that further restricts permissions for the IAM Role being assumed. +* `policy_arns` - (Optional) A set of Amazon Resource Names (ARNs) for IAM Policies that further limit permissions for the assumed IAM Role. +* `session_name` - (Optional) The session name to be used when assuming the role. +* `tags` - (Optional) A map of tags to be associated with the assumed role session. +* `transitive_tag_keys` - (Optional) A set of tag keys from the assumed role session to be passed to any subsequent sessions. + +The following arguments on the top level are deprecated: * `assume_role_duration_seconds` - (Optional) Number of seconds to restrict the assume role session duration. + Use `assume_role.duration` instead. * `assume_role_policy` - (Optional) IAM Policy JSON describing further restricting permissions for the IAM Role being assumed. + Use `assume_role.policy` instead. * `assume_role_policy_arns` - (Optional) Set of Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed. + Use `assume_role.policy_arns` instead. * `assume_role_tags` - (Optional) Map of assume role session tags. + Use `assume_role.tags` instead. * `assume_role_transitive_tag_keys` - (Optional) Set of assume role session tag keys to pass to any subsequent sessions. + Use `assume_role.transitive_tag_keys` instead. * `external_id` - (Optional) External identifier to use when assuming the role. + Use `assume_role.external_id` instead. * `role_arn` - (Optional) Amazon Resource Name (ARN) of the IAM Role to assume. + Use `assume_role.role_arn` instead. * `session_name` - (Optional) Session name to use when assuming the role. + Use `assume_role.session_name` instead. + +```hcl +terraform { + backend "s3" { + bucket = "mybucket" + key = "my/key.tfstate" + region = "us-east-1" + assume_role = { + role_arn = "arn:aws:iam::ACCOUNT-ID:role/Opentofu" + } + } +} +``` ### S3 State Storage