// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package s3 import ( "context" "encoding/base64" "fmt" "log" "os" "regexp" "sort" "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/s3" awsbase "github.com/hashicorp/aws-sdk-go-base/v2" baselogging "github.com/hashicorp/aws-sdk-go-base/v2/logging" awsbaseValidation "github.com/hashicorp/aws-sdk-go-base/v2/validation" "github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/version" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/gocty" ) func New(enc encryption.StateEncryption) backend.Backend { return &Backend{encryption: enc} } type Backend struct { encryption encryption.StateEncryption s3Client *s3.Client dynClient *dynamodb.Client awsConfig aws.Config bucketName string keyName string serverSideEncryption bool customerEncryptionKey []byte acl string kmsKeyID string ddbTable string workspaceKeyPrefix string skipS3Checksum bool useLockfile bool } // ConfigSchema returns a description of the expected configuration // structure for the receiving backend. // This structure is mirrored by the encryption aws_kms key provider and should be kept in sync. func (b *Backend) ConfigSchema() *configschema.Block { return &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "bucket": { Type: cty.String, Required: true, Description: "The name of the S3 bucket", }, "key": { Type: cty.String, Required: true, Description: "The path to the state file inside the bucket", }, "region": { Type: cty.String, Optional: true, Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).", }, "endpoints": { Optional: true, NestedType: &configschema.Object{ Nesting: configschema.NestingSingle, Attributes: map[string]*configschema.Attribute{ "s3": { Type: cty.String, Optional: true, Description: "A custom endpoint for the S3 API.", }, "iam": { Type: cty.String, Optional: true, Description: "A custom endpoint for the IAM API.", }, "sts": { Type: cty.String, Optional: true, Description: "A custom endpoint for the STS API.", }, "dynamodb": { Type: cty.String, Optional: true, Description: "A custom endpoint for the DynamoDB API.", }, }, }, }, "dynamodb_endpoint": { Type: cty.String, Optional: true, Description: "A custom endpoint for the DynamoDB API. Use `endpoints.dynamodb` instead.", Deprecated: true, }, "endpoint": { Type: cty.String, Optional: true, Description: "A custom endpoint for the S3 API. Use `endpoints.s3` instead", Deprecated: true, }, "iam_endpoint": { Type: cty.String, Optional: true, Description: "A custom endpoint for the IAM API. Use `endpoints.iam` instead", Deprecated: true, }, "sts_endpoint": { Type: cty.String, Optional: true, Description: "A custom endpoint for the STS API. Use `endpoints.sts` instead", Deprecated: true, }, "sts_region": { Type: cty.String, Optional: true, Description: "The region where AWS STS operations will take place", }, "encrypt": { Type: cty.Bool, Optional: true, Description: "Whether to enable server side encryption of the state file", }, "acl": { Type: cty.String, Optional: true, Description: "Canned ACL to be applied to the state file", }, "access_key": { Type: cty.String, Optional: true, Description: "AWS access key", }, "secret_key": { Type: cty.String, Optional: true, Description: "AWS secret key", }, "kms_key_id": { Type: cty.String, Optional: true, Description: "The ARN of a KMS Key to use for encrypting the state", }, "dynamodb_table": { Type: cty.String, Optional: true, Description: "DynamoDB table for state locking and consistency", }, "profile": { Type: cty.String, Optional: true, Description: "AWS profile name", }, "shared_credentials_file": { Type: cty.String, Optional: true, Description: "Path to a shared credentials file", }, "shared_credentials_files": { Type: cty.Set(cty.String), Optional: true, Description: "Paths to a shared credentials files", }, "shared_config_files": { Type: cty.Set(cty.String), Optional: true, Description: "Paths to shared config files", }, "token": { Type: cty.String, Optional: true, Description: "MFA token", }, "skip_credentials_validation": { Type: cty.Bool, Optional: true, Description: "Skip the credentials validation via STS API.", }, "skip_metadata_api_check": { Type: cty.Bool, Optional: true, Description: "Skip the AWS Metadata API check.", }, "skip_region_validation": { Type: cty.Bool, Optional: true, Description: "Skip static validation of region name.", }, "skip_requesting_account_id": { Type: cty.Bool, Optional: true, Description: "Skip requesting the account ID. Useful for AWS API implementations that do not have the IAM, STS API, or metadata API.", }, "sse_customer_key": { Type: cty.String, Optional: true, Description: "The base64-encoded encryption key to use for server-side encryption with customer-provided keys (SSE-C).", Sensitive: true, }, "role_arn": { 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": { Type: cty.String, Optional: true, Description: "The prefix applied to the non-default state path inside the bucket.", }, "force_path_style": { Type: cty.Bool, Optional: true, Description: "Force s3 to use path style api. Use `use_path_style` instead.", Deprecated: true, }, "use_path_style": { Type: cty.Bool, Optional: true, Description: "Enable path-style S3 URLs.", }, "retry_mode": { Type: cty.String, Optional: true, Description: "Specifies how retries are attempted. Valid values are `standard` and `adaptive`.", }, "max_retries": { Type: cty.Number, Optional: true, Description: "The maximum number of times an AWS API request is retried on retryable failure.", }, "custom_ca_bundle": { Type: cty.String, Optional: true, Description: "File containing custom root and intermediate certificates. Can also be configured using the `AWS_CA_BUNDLE` environment variable.", }, "ec2_metadata_service_endpoint": { Type: cty.String, Optional: true, Description: "The endpoint of IMDS.", }, "ec2_metadata_service_endpoint_mode": { Type: cty.String, Optional: true, Description: "The endpoint mode of IMDS. Valid values: IPv4, IPv6.", }, "assume_role": { Optional: true, 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, // }, // }, }, }, }, "assume_role_with_web_identity": { Optional: true, NestedType: &configschema.Object{ Nesting: configschema.NestingSingle, Attributes: map[string]*configschema.Attribute{ "role_arn": { Type: cty.String, Optional: true, Description: "The Amazon Resource Name (ARN) role to assume.", }, "web_identity_token": { Type: cty.String, Optional: true, Sensitive: true, Description: "The OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.", }, "web_identity_token_file": { Type: cty.String, Optional: true, Description: "The path to a file which contains an OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.", }, "session_name": { Type: cty.String, Optional: true, Description: "The name applied to this assume-role session.", }, "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.", }, "duration": { 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.", }, }, }, }, "forbidden_account_ids": { Type: cty.Set(cty.String), Optional: true, Description: "List of forbidden AWS account IDs.", }, "allowed_account_ids": { Type: cty.Set(cty.String), Optional: true, Description: "List of allowed AWS account IDs.", }, "http_proxy": { Type: cty.String, Optional: true, Description: "The address of an HTTP proxy to use when accessing the AWS API.", }, "https_proxy": { Type: cty.String, Optional: true, Description: "The address of an HTTPS proxy to use when accessing the AWS API.", }, "no_proxy": { Type: cty.String, Optional: true, Description: `Comma-separated values which specify hosts that should be excluded from proxying. See details: https://cs.opensource.google/go/x/net/+/refs/tags/v0.17.0:http/httpproxy/proxy.go;l=38-50.`, }, "insecure": { Type: cty.Bool, Optional: true, Description: "Explicitly allow the backend to perform \"insecure\" SSL requests.", }, "use_dualstack_endpoint": { Type: cty.Bool, Optional: true, Description: "Resolve an endpoint with DualStack capability.", }, "use_fips_endpoint": { Type: cty.Bool, Optional: true, Description: "Resolve an endpoint with FIPS capability.", }, "skip_s3_checksum": { Type: cty.Bool, Optional: true, Description: "Do not include checksum when uploading S3 Objects. Useful for some S3-Compatible APIs as some of them do not support checksum checks.", }, "use_lockfile": { Type: cty.Bool, Optional: true, Description: "Manage locking in the same configured S3 bucket", }, }, } } // 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 } if val := obj.GetAttr("bucket"); val.IsNull() || val.AsString() == "" { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid bucket value", `The "bucket" attribute value must not be empty.`, cty.Path{cty.GetAttrStep{Name: "bucket"}}, )) } if val := obj.GetAttr("key"); val.IsNull() || val.AsString() == "" { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid key value", `The "key" attribute value must not be empty.`, cty.Path{cty.GetAttrStep{Name: "key"}}, )) } else if strings.HasPrefix(val.AsString(), "/") || strings.HasSuffix(val.AsString(), "/") { // S3 will strip leading slashes from an object, so while this will // technically be accepted by S3, it will break our workspace hierarchy. // S3 will recognize objects with a trailing slash as a directory // so they should not be valid keys diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid key value", `The "key" attribute value must not start or end with with "/".`, cty.Path{cty.GetAttrStep{Name: "key"}}, )) } 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.Path{cty.GetAttrStep{Name: "region"}}, )) } } if val := obj.GetAttr("kms_key_id"); !val.IsNull() && val.AsString() != "" { if val := obj.GetAttr("sse_customer_key"); !val.IsNull() && val.AsString() != "" { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid encryption configuration", encryptionKeyConflictError, cty.Path{}, )) } else if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid encryption configuration", encryptionKeyConflictEnvVarError, cty.Path{}, )) } diags = diags.Append(validateKMSKey(cty.Path{cty.GetAttrStep{Name: "kms_key_id"}}, val.AsString())) } if val := obj.GetAttr("workspace_key_prefix"); !val.IsNull() { if v := val.AsString(); strings.HasPrefix(v, "/") || strings.HasSuffix(v, "/") { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid workspace_key_prefix value", `The "workspace_key_prefix" attribute value must not start with "/".`, cty.Path{cty.GetAttrStep{Name: "workspace_key_prefix"}}, )) } } 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() { detail := fmt.Sprintf( `Parameter "%s" is deprecated. Use "%s" instead.`, pathString(attrPath), pathString(cty.GetAttrPath("shared_credentials_files"))) diags = diags.Append(attributeWarningDiag( "Deprecated Parameter", detail, attrPath)) } if val := obj.GetAttr("force_path_style"); !val.IsNull() { attrPath := cty.GetAttrPath("force_path_style") detail := fmt.Sprintf( `Parameter "%s" is deprecated. Use "%s" instead.`, pathString(attrPath), pathString(cty.GetAttrPath("use_path_style"))) diags = diags.Append(attributeWarningDiag( "Deprecated Parameter", detail, attrPath)) } validateAttributesConflict( cty.GetAttrPath("force_path_style"), cty.GetAttrPath("use_path_style"), )(obj, cty.Path{}, &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() { 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), )) } } if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { diags = diags.Append(validateAssumeRoleWithWebIdentity(val, cty.GetAttrPath("assume_role_with_web_identity"))) } validateAttributesConflict( cty.GetAttrPath("allowed_account_ids"), cty.GetAttrPath("forbidden_account_ids"), )(obj, cty.Path{}, &diags) if val := obj.GetAttr("retry_mode"); !val.IsNull() { s := val.AsString() if _, err := aws.ParseRetryMode(s); err != nil { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid retry mode", fmt.Sprintf("Valid values are %q and %q.", aws.RetryModeStandard, aws.RetryModeAdaptive), cty.Path{cty.GetAttrStep{Name: "retry_mode"}}, )) } } for _, endpoint := range customEndpoints { endpoint.Validate(obj, &diags) } return obj, diags } // Configure uses the provided configuration to set configuration fields // within the backend. // // The given configuration is assumed to have already been validated // against the schema returned by ConfigSchema and passed validation // via PrepareConfig. func (b *Backend) Configure(ctx context.Context, obj cty.Value) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if obj.IsNull() { return diags } var region string if v, ok := stringAttrOk(obj, "region"); ok { region = v } if region != "" && !boolAttr(obj, "skip_region_validation") { if err := awsbaseValidation.SupportedRegion(region); err != nil { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid region value", err.Error(), cty.Path{cty.GetAttrStep{Name: "region"}}, )) return diags } } b.bucketName = stringAttr(obj, "bucket") b.keyName = stringAttr(obj, "key") b.acl = stringAttr(obj, "acl") b.workspaceKeyPrefix = stringAttrDefault(obj, "workspace_key_prefix", "env:") b.serverSideEncryption = boolAttr(obj, "encrypt") b.kmsKeyID = stringAttr(obj, "kms_key_id") b.ddbTable = stringAttr(obj, "dynamodb_table") b.useLockfile = boolAttr(obj, "use_lockfile") b.skipS3Checksum = boolAttr(obj, "skip_s3_checksum") if customerKey, ok := stringAttrOk(obj, "sse_customer_key"); ok { if len(customerKey) != 44 { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid sse_customer_key value", "sse_customer_key must be 44 characters in length", cty.Path{cty.GetAttrStep{Name: "sse_customer_key"}}, )) } else { var err error if b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKey); err != nil { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Invalid sse_customer_key value", fmt.Sprintf("sse_customer_key must be base64 encoded: %s", err), cty.Path{cty.GetAttrStep{Name: "sse_customer_key"}}, )) } } } else if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" { if len(customerKey) != 44 { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid AWS_SSE_CUSTOMER_KEY value", `The environment variable "AWS_SSE_CUSTOMER_KEY" must be 44 characters in length`, )) } else { var err error if b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKey); err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid AWS_SSE_CUSTOMER_KEY value", fmt.Sprintf(`The environment variable "AWS_SSE_CUSTOMER_KEY" must be base64 encoded: %s`, err), )) } } } ctx, baselog := attachLoggerToContext(ctx) cfg := &awsbase.Config{ AccessKey: stringAttr(obj, "access_key"), CallerDocumentationURL: "https://opentofu.org/docs/language/settings/backends/s3", CallerName: "S3 Backend", IamEndpoint: customEndpoints["iam"].String(obj), MaxRetries: intAttrDefault(obj, "max_retries", 5), Profile: stringAttr(obj, "profile"), Region: stringAttr(obj, "region"), SecretKey: stringAttr(obj, "secret_key"), SkipCredsValidation: boolAttr(obj, "skip_credentials_validation"), SkipRequestingAccountId: boolAttr(obj, "skip_requesting_account_id"), StsEndpoint: customEndpoints["sts"].String(obj), StsRegion: stringAttr(obj, "sts_region"), Token: stringAttr(obj, "token"), // Note: we don't need to read env variables explicitly because they are read implicitly by aws-sdk-base-go: // see: https://github.com/hashicorp/aws-sdk-go-base/blob/v2.0.0-beta.41/internal/config/config.go#L133 // which relies on: https://cs.opensource.google/go/x/net/+/refs/tags/v0.18.0:http/httpproxy/proxy.go;l=89-96 // // Note: we are switching to "separate" mode here since the legacy mode is deprecated and should no longer be // used. HTTPProxyMode: awsbase.HTTPProxyModeSeparate, Insecure: boolAttr(obj, "insecure"), UseDualStackEndpoint: boolAttr(obj, "use_dualstack_endpoint"), UseFIPSEndpoint: boolAttr(obj, "use_fips_endpoint"), UserAgent: awsbase.UserAgentProducts{ {Name: "APN", Version: "1.0"}, {Name: httpclient.DefaultApplicationName, Version: version.String()}, }, CustomCABundle: stringAttrDefaultEnvVar(obj, "custom_ca_bundle", "AWS_CA_BUNDLE"), EC2MetadataServiceEndpoint: stringAttrDefaultEnvVar(obj, "ec2_metadata_service_endpoint", "AWS_EC2_METADATA_SERVICE_ENDPOINT"), EC2MetadataServiceEndpointMode: stringAttrDefaultEnvVar(obj, "ec2_metadata_service_endpoint_mode", "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE"), Logger: baselog, } if val, ok := stringAttrOk(obj, "http_proxy"); ok { cfg.HTTPProxy = &val } if val, ok := stringAttrOk(obj, "https_proxy"); ok { cfg.HTTPSProxy = &val } if val, ok := stringAttrOk(obj, "no_proxy"); ok { cfg.NoProxy = val } if val, ok := boolAttrOk(obj, "skip_metadata_api_check"); ok { if val { cfg.EC2MetadataServiceEnableState = imds.ClientDisabled } else { cfg.EC2MetadataServiceEnableState = imds.ClientEnabled } } if val, ok := stringAttrOk(obj, "shared_credentials_file"); ok { cfg.SharedCredentialsFiles = []string{val} } if value := obj.GetAttr("assume_role"); !value.IsNull() { cfg.AssumeRole = []awsbase.AssumeRole{configureNestedAssumeRole(obj)} } else if value := obj.GetAttr("role_arn"); !value.IsNull() { cfg.AssumeRole = []awsbase.AssumeRole{configureAssumeRole(obj)} } if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { cfg.AssumeRoleWithWebIdentity = configureAssumeRoleWithWebIdentity(val) } if val, ok := stringSliceAttrDefaultEnvVarOk(obj, "shared_credentials_files", "AWS_SHARED_CREDENTIALS_FILE"); ok { cfg.SharedCredentialsFiles = val } if val, ok := stringSliceAttrDefaultEnvVarOk(obj, "shared_config_files", "AWS_SHARED_CONFIG_FILE"); ok { cfg.SharedConfigFiles = val } if val, ok := stringSliceAttrOk(obj, "allowed_account_ids"); ok { cfg.AllowedAccountIds = val } if val, ok := stringSliceAttrOk(obj, "forbidden_account_ids"); ok { cfg.ForbiddenAccountIds = val } if val, ok := stringAttrOk(obj, "retry_mode"); ok { mode, err := aws.ParseRetryMode(val) if err != nil { panic(fmt.Sprintf("invalid retry mode %q: %s", val, err)) } cfg.RetryMode = mode } _, awsConfig, awsDiags := awsbase.GetAwsConfig(ctx, cfg) for _, d := range awsDiags { diags = diags.Append(tfdiags.Sourceless( baseSeverityToTofuSeverity(d.Severity()), d.Summary(), d.Detail(), )) } if d := verifyAllowedAccountID(ctx, awsConfig, cfg); len(d) != 0 { diags = diags.Append(d) } if diags.HasErrors() { return diags } b.awsConfig = awsConfig b.dynClient = dynamodb.NewFromConfig(awsConfig, getDynamoDBConfig(obj)) b.s3Client = s3.NewFromConfig(awsConfig, getS3Config(obj)) return diags } func attachLoggerToContext(ctx context.Context) (context.Context, baselogging.HcLogger) { ctx, baselog := baselogging.NewHcLogger(ctx, logging.HCLogger().Named("backend-s3")) ctx = baselogging.RegisterLogger(ctx, baselog) return ctx, baselog } func verifyAllowedAccountID(ctx context.Context, awsConfig aws.Config, cfg *awsbase.Config) tfdiags.Diagnostics { if len(cfg.ForbiddenAccountIds) == 0 && len(cfg.AllowedAccountIds) == 0 { return nil } var diags tfdiags.Diagnostics accountID, _, awsDiags := awsbase.GetAwsAccountIDAndPartition(ctx, awsConfig, cfg) for _, d := range awsDiags { diags = diags.Append(tfdiags.Sourceless( baseSeverityToTofuSeverity(d.Severity()), fmt.Sprintf("Retrieving AWS account details: %s", d.Summary()), d.Detail(), )) } err := cfg.VerifyAccountIDAllowed(accountID) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid account ID", err.Error(), )) } return diags } func getDynamoDBConfig(obj cty.Value) func(options *dynamodb.Options) { return func(options *dynamodb.Options) { if v, ok := customEndpoints["dynamodb"].StringOk(obj); ok { options.BaseEndpoint = aws.String(v) } } } func getS3Config(obj cty.Value) func(options *s3.Options) { return func(options *s3.Options) { if v, ok := customEndpoints["s3"].StringOk(obj); ok { options.BaseEndpoint = aws.String(v) } if v, ok := boolAttrOk(obj, "force_path_style"); ok { options.UsePathStyle = v } if v, ok := boolAttrOk(obj, "use_path_style"); ok { options.UsePathStyle = v } } } 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{} assumeRole.RoleARN = stringAttr(obj, "role_arn") assumeRole.Duration = time.Duration(int64(intAttr(obj, "assume_role_duration_seconds")) * int64(time.Second)) assumeRole.ExternalID = stringAttr(obj, "external_id") assumeRole.Policy = stringAttr(obj, "assume_role_policy") assumeRole.SessionName = stringAttr(obj, "session_name") if val, ok := stringSliceAttrOk(obj, "assume_role_policy_arns"); ok { assumeRole.PolicyARNs = val } if val, ok := stringMapAttrOk(obj, "assume_role_tags"); ok { assumeRole.Tags = val } if val, ok := stringSliceAttrOk(obj, "assume_role_transitive_tag_keys"); ok { assumeRole.TransitiveTagKeys = val } return assumeRole } func configureAssumeRoleWithWebIdentity(obj cty.Value) *awsbase.AssumeRoleWithWebIdentity { cfg := &awsbase.AssumeRoleWithWebIdentity{ RoleARN: stringAttrDefaultEnvVar(obj, "role_arn", "AWS_ROLE_ARN"), Policy: stringAttr(obj, "policy"), PolicyARNs: stringSliceAttr(obj, "policy_arns"), SessionName: stringAttrDefaultEnvVar(obj, "session_name", "AWS_ROLE_SESSION_NAME"), WebIdentityToken: stringAttrDefaultEnvVar(obj, "web_identity_token", "AWS_WEB_IDENTITY_TOKEN"), WebIdentityTokenFile: stringAttrDefaultEnvVar(obj, "web_identity_token_file", "AWS_WEB_IDENTITY_TOKEN_FILE"), } if val, ok := stringAttrOk(obj, "duration"); ok { d, 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)) } cfg.Duration = d } return cfg } func stringValue(val cty.Value) string { v, _ := stringValueOk(val) return v } func stringValueOk(val cty.Value) (string, bool) { if val.IsNull() { return "", false } else { return val.AsString(), true } } func stringAttr(obj cty.Value, name string) string { return stringValue(obj.GetAttr(name)) } func stringAttrOk(obj cty.Value, name string) (string, bool) { return stringValueOk(obj.GetAttr(name)) } func stringAttrDefault(obj cty.Value, name, def string) string { if v, ok := stringAttrOk(obj, name); !ok { return def } else { return v } } func stringSliceValue(val cty.Value) []string { v, _ := stringSliceValueOk(val) return v } func stringSliceValueOk(val cty.Value) ([]string, bool) { if val.IsNull() { return nil, false } var v []string if err := gocty.FromCtyValue(val, &v); err != nil { return nil, false } return v, true } func stringSliceAttr(obj cty.Value, name string) []string { return stringSliceValue(obj.GetAttr(name)) } func stringSliceAttrOk(obj cty.Value, name string) ([]string, bool) { return stringSliceValueOk(obj.GetAttr(name)) } func stringSliceAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) ([]string, bool) { if v, ok := stringSliceAttrOk(obj, name); !ok { for _, envvar := range envvars { if ev := os.Getenv(envvar); ev != "" { return []string{ev}, true } } return nil, false } else { return v, true } } func stringAttrDefaultEnvVar(obj cty.Value, name string, envvars ...string) string { if v, ok := stringAttrDefaultEnvVarOk(obj, name, envvars...); !ok { return "" } else { return v } } func stringAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) (string, bool) { if v, ok := stringAttrOk(obj, name); !ok { for _, envvar := range envvars { if v := os.Getenv(envvar); v != "" { return v, true } } return "", false } else { return v, true } } func boolAttr(obj cty.Value, name string) bool { v, _ := boolAttrOk(obj, name) return v } func boolAttrOk(obj cty.Value, name string) (bool, bool) { if val := obj.GetAttr(name); val.IsNull() { return false, false } else { return val.True(), true } } func intAttr(obj cty.Value, name string) int { v, _ := intAttrOk(obj, name) return v } func intAttrOk(obj cty.Value, name string) (int, bool) { if val := obj.GetAttr(name); val.IsNull() { return 0, false } else { var v int if err := gocty.FromCtyValue(val, &v); err != nil { return 0, false } return v, true } } func intAttrDefault(obj cty.Value, name string, def int) int { if v, ok := intAttrOk(obj, name); !ok { return def } else { return v } } 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 customEndpointAttrDefaultEnvVarOk(obj cty.Value, endpointsKey, deprecatedKey string, envvars ...string) (string, bool) { if val := obj.GetAttr("endpoints"); !val.IsNull() { if v, ok := stringAttrDefaultEnvVarOk(val, endpointsKey, envvars...); ok { return v, true } } return stringAttrDefaultEnvVarOk(obj, deprecatedKey, envvars...) } func pathString(path cty.Path) string { var buf strings.Builder for i, step := range path { switch x := step.(type) { case cty.GetAttrStep: if i != 0 { buf.WriteString(".") } buf.WriteString(x.Name) case cty.IndexStep: val := x.Key typ := val.Type() var s string switch { case typ == cty.String: s = val.AsString() case typ == cty.Number: num := val.AsBigFloat() if num.IsInt() { s = num.Text('f', -1) } else { s = num.String() } default: s = fmt.Sprintf("", typ.FriendlyName()) } buf.WriteString(fmt.Sprintf("[%s]", s)) default: if i != 0 { buf.WriteString(".") } buf.WriteString(fmt.Sprintf("", x)) } } 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) while "sse_customer_key" is used for encryption with customer-managed keys (SSE-C). Please choose one or the other.` const encryptionKeyConflictEnvVarError = `Only one of "kms_key_id" and the environment variable "AWS_SSE_CUSTOMER_KEY" can be set. 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.` type customEndpoint struct { Paths []cty.Path EnvVars []string } func (e customEndpoint) Validate(obj cty.Value, diags *tfdiags.Diagnostics) { validateAttributesConflict(e.Paths...)(obj, cty.Path{}, diags) } func (e customEndpoint) String(obj cty.Value) string { v, _ := e.StringOk(obj) return v } func includeProtoIfNecessary(endpoint string) string { if matched, _ := regexp.MatchString("[a-z]*://.*", endpoint); !matched { log.Printf("[DEBUG] Adding https:// prefix to endpoint '%s'", endpoint) endpoint = fmt.Sprintf("https://%s", endpoint) } return endpoint } func (e customEndpoint) StringOk(obj cty.Value) (string, bool) { for _, path := range e.Paths { val, err := path.Apply(obj) if err != nil { continue } if s, ok := stringValueOk(val); ok { return includeProtoIfNecessary(s), true } } for _, envVar := range e.EnvVars { if v := os.Getenv(envVar); v != "" { return includeProtoIfNecessary(v), true } } return "", false } var customEndpoints = map[string]customEndpoint{ "s3": { Paths: []cty.Path{ cty.GetAttrPath("endpoints").GetAttr("s3"), cty.GetAttrPath("endpoint"), }, EnvVars: []string{ "AWS_ENDPOINT_URL_S3", "AWS_S3_ENDPOINT", }, }, "iam": { Paths: []cty.Path{ cty.GetAttrPath("endpoints").GetAttr("iam"), cty.GetAttrPath("iam_endpoint"), }, EnvVars: []string{ "AWS_ENDPOINT_URL_IAM", "AWS_IAM_ENDPOINT", }, }, "sts": { Paths: []cty.Path{ cty.GetAttrPath("endpoints").GetAttr("sts"), cty.GetAttrPath("sts_endpoint"), }, EnvVars: []string{ "AWS_ENDPOINT_URL_STS", "AWS_STS_ENDPOINT", }, }, "dynamodb": { Paths: []cty.Path{ cty.GetAttrPath("endpoints").GetAttr("dynamodb"), cty.GetAttrPath("dynamodb_endpoint"), }, EnvVars: []string{ "AWS_ENDPOINT_URL_DYNAMODB", "AWS_DYNAMODB_ENDPOINT", }, }, }