github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/s3/backend.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package s3 7 8 import ( 9 "context" 10 "encoding/base64" 11 "fmt" 12 "log" 13 "os" 14 "regexp" 15 "sort" 16 "strings" 17 "time" 18 19 "github.com/aws/aws-sdk-go-v2/aws" 20 "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" 21 "github.com/aws/aws-sdk-go-v2/service/dynamodb" 22 "github.com/aws/aws-sdk-go-v2/service/s3" 23 awsbase "github.com/hashicorp/aws-sdk-go-base/v2" 24 baselogging "github.com/hashicorp/aws-sdk-go-base/v2/logging" 25 awsbaseValidation "github.com/hashicorp/aws-sdk-go-base/v2/validation" 26 "github.com/opentofu/opentofu/internal/backend" 27 "github.com/opentofu/opentofu/internal/configs/configschema" 28 "github.com/opentofu/opentofu/internal/encryption" 29 "github.com/opentofu/opentofu/internal/httpclient" 30 "github.com/opentofu/opentofu/internal/logging" 31 "github.com/opentofu/opentofu/internal/tfdiags" 32 "github.com/opentofu/opentofu/version" 33 "github.com/zclconf/go-cty/cty" 34 "github.com/zclconf/go-cty/cty/gocty" 35 ) 36 37 func New(enc encryption.StateEncryption) backend.Backend { 38 return &Backend{encryption: enc} 39 } 40 41 type Backend struct { 42 encryption encryption.StateEncryption 43 s3Client *s3.Client 44 dynClient *dynamodb.Client 45 awsConfig aws.Config 46 47 bucketName string 48 keyName string 49 serverSideEncryption bool 50 customerEncryptionKey []byte 51 acl string 52 kmsKeyID string 53 ddbTable string 54 workspaceKeyPrefix string 55 skipS3Checksum bool 56 } 57 58 // ConfigSchema returns a description of the expected configuration 59 // structure for the receiving backend. 60 // This structure is mirrored by the encryption aws_kms key provider and should be kept in sync. 61 func (b *Backend) ConfigSchema() *configschema.Block { 62 return &configschema.Block{ 63 Attributes: map[string]*configschema.Attribute{ 64 "bucket": { 65 Type: cty.String, 66 Required: true, 67 Description: "The name of the S3 bucket", 68 }, 69 "key": { 70 Type: cty.String, 71 Required: true, 72 Description: "The path to the state file inside the bucket", 73 }, 74 "region": { 75 Type: cty.String, 76 Optional: true, 77 Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).", 78 }, 79 "endpoints": { 80 Optional: true, 81 NestedType: &configschema.Object{ 82 Nesting: configschema.NestingSingle, 83 Attributes: map[string]*configschema.Attribute{ 84 "s3": { 85 Type: cty.String, 86 Optional: true, 87 Description: "A custom endpoint for the S3 API.", 88 }, 89 "iam": { 90 Type: cty.String, 91 Optional: true, 92 Description: "A custom endpoint for the IAM API.", 93 }, 94 "sts": { 95 Type: cty.String, 96 Optional: true, 97 Description: "A custom endpoint for the STS API.", 98 }, 99 "dynamodb": { 100 Type: cty.String, 101 Optional: true, 102 Description: "A custom endpoint for the DynamoDB API.", 103 }, 104 }, 105 }, 106 }, 107 "dynamodb_endpoint": { 108 Type: cty.String, 109 Optional: true, 110 Description: "A custom endpoint for the DynamoDB API. Use `endpoints.dynamodb` instead.", 111 Deprecated: true, 112 }, 113 "endpoint": { 114 Type: cty.String, 115 Optional: true, 116 Description: "A custom endpoint for the S3 API. Use `endpoints.s3` instead", 117 Deprecated: true, 118 }, 119 "iam_endpoint": { 120 Type: cty.String, 121 Optional: true, 122 Description: "A custom endpoint for the IAM API. Use `endpoints.iam` instead", 123 Deprecated: true, 124 }, 125 "sts_endpoint": { 126 Type: cty.String, 127 Optional: true, 128 Description: "A custom endpoint for the STS API. Use `endpoints.sts` instead", 129 Deprecated: true, 130 }, 131 "sts_region": { 132 Type: cty.String, 133 Optional: true, 134 Description: "The region where AWS STS operations will take place", 135 }, 136 "encrypt": { 137 Type: cty.Bool, 138 Optional: true, 139 Description: "Whether to enable server side encryption of the state file", 140 }, 141 "acl": { 142 Type: cty.String, 143 Optional: true, 144 Description: "Canned ACL to be applied to the state file", 145 }, 146 "access_key": { 147 Type: cty.String, 148 Optional: true, 149 Description: "AWS access key", 150 }, 151 "secret_key": { 152 Type: cty.String, 153 Optional: true, 154 Description: "AWS secret key", 155 }, 156 "kms_key_id": { 157 Type: cty.String, 158 Optional: true, 159 Description: "The ARN of a KMS Key to use for encrypting the state", 160 }, 161 "dynamodb_table": { 162 Type: cty.String, 163 Optional: true, 164 Description: "DynamoDB table for state locking and consistency", 165 }, 166 "profile": { 167 Type: cty.String, 168 Optional: true, 169 Description: "AWS profile name", 170 }, 171 "shared_credentials_file": { 172 Type: cty.String, 173 Optional: true, 174 Description: "Path to a shared credentials file", 175 }, 176 "shared_credentials_files": { 177 Type: cty.Set(cty.String), 178 Optional: true, 179 Description: "Paths to a shared credentials files", 180 }, 181 "shared_config_files": { 182 Type: cty.Set(cty.String), 183 Optional: true, 184 Description: "Paths to shared config files", 185 }, 186 "token": { 187 Type: cty.String, 188 Optional: true, 189 Description: "MFA token", 190 }, 191 "skip_credentials_validation": { 192 Type: cty.Bool, 193 Optional: true, 194 Description: "Skip the credentials validation via STS API.", 195 }, 196 "skip_metadata_api_check": { 197 Type: cty.Bool, 198 Optional: true, 199 Description: "Skip the AWS Metadata API check.", 200 }, 201 "skip_region_validation": { 202 Type: cty.Bool, 203 Optional: true, 204 Description: "Skip static validation of region name.", 205 }, 206 "skip_requesting_account_id": { 207 Type: cty.Bool, 208 Optional: true, 209 Description: "Skip requesting the account ID. Useful for AWS API implementations that do not have the IAM, STS API, or metadata API.", 210 }, 211 "sse_customer_key": { 212 Type: cty.String, 213 Optional: true, 214 Description: "The base64-encoded encryption key to use for server-side encryption with customer-provided keys (SSE-C).", 215 Sensitive: true, 216 }, 217 "role_arn": { 218 Type: cty.String, 219 Optional: true, 220 Description: "The role to be assumed", 221 Deprecated: true, 222 }, 223 "session_name": { 224 Type: cty.String, 225 Optional: true, 226 Description: "The session name to use when assuming the role.", 227 Deprecated: true, 228 }, 229 "external_id": { 230 Type: cty.String, 231 Optional: true, 232 Description: "The external ID to use when assuming the role", 233 Deprecated: true, 234 }, 235 "assume_role_duration_seconds": { 236 Type: cty.Number, 237 Optional: true, 238 Description: "Seconds to restrict the assume role session duration.", 239 Deprecated: true, 240 }, 241 "assume_role_policy": { 242 Type: cty.String, 243 Optional: true, 244 Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", 245 Deprecated: true, 246 }, 247 "assume_role_policy_arns": { 248 Type: cty.Set(cty.String), 249 Optional: true, 250 Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", 251 Deprecated: true, 252 }, 253 "assume_role_tags": { 254 Type: cty.Map(cty.String), 255 Optional: true, 256 Description: "Assume role session tags.", 257 Deprecated: true, 258 }, 259 "assume_role_transitive_tag_keys": { 260 Type: cty.Set(cty.String), 261 Optional: true, 262 Description: "Assume role session tag keys to pass to any subsequent sessions.", 263 Deprecated: true, 264 }, 265 "workspace_key_prefix": { 266 Type: cty.String, 267 Optional: true, 268 Description: "The prefix applied to the non-default state path inside the bucket.", 269 }, 270 "force_path_style": { 271 Type: cty.Bool, 272 Optional: true, 273 Description: "Force s3 to use path style api. Use `use_path_style` instead.", 274 Deprecated: true, 275 }, 276 "use_path_style": { 277 Type: cty.Bool, 278 Optional: true, 279 Description: "Enable path-style S3 URLs.", 280 }, 281 "retry_mode": { 282 Type: cty.String, 283 Optional: true, 284 Description: "Specifies how retries are attempted. Valid values are `standard` and `adaptive`.", 285 }, 286 "max_retries": { 287 Type: cty.Number, 288 Optional: true, 289 Description: "The maximum number of times an AWS API request is retried on retryable failure.", 290 }, 291 "use_legacy_workflow": { 292 Type: cty.Bool, 293 Optional: true, 294 Description: "Use the legacy authentication workflow, preferring environment variables over backend configuration.", 295 Deprecated: true, 296 }, 297 "custom_ca_bundle": { 298 Type: cty.String, 299 Optional: true, 300 Description: "File containing custom root and intermediate certificates. Can also be configured using the `AWS_CA_BUNDLE` environment variable.", 301 }, 302 "ec2_metadata_service_endpoint": { 303 Type: cty.String, 304 Optional: true, 305 Description: "The endpoint of IMDS.", 306 }, 307 "ec2_metadata_service_endpoint_mode": { 308 Type: cty.String, 309 Optional: true, 310 Description: "The endpoint mode of IMDS. Valid values: IPv4, IPv6.", 311 }, 312 "assume_role": { 313 Optional: true, 314 NestedType: &configschema.Object{ 315 Nesting: configschema.NestingSingle, 316 Attributes: map[string]*configschema.Attribute{ 317 "role_arn": { 318 Type: cty.String, 319 Required: true, 320 Description: "The role to be assumed.", 321 }, 322 "duration": { 323 Type: cty.String, 324 Optional: true, 325 Description: "Seconds to restrict the assume role session duration.", 326 }, 327 "external_id": { 328 Type: cty.String, 329 Optional: true, 330 Description: "The external ID to use when assuming the role", 331 }, 332 "policy": { 333 Type: cty.String, 334 Optional: true, 335 Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", 336 }, 337 "policy_arns": { 338 Type: cty.Set(cty.String), 339 Optional: true, 340 Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", 341 }, 342 "session_name": { 343 Type: cty.String, 344 Optional: true, 345 Description: "The session name to use when assuming the role.", 346 }, 347 "tags": { 348 Type: cty.Map(cty.String), 349 Optional: true, 350 Description: "Assume role session tags.", 351 }, 352 "transitive_tag_keys": { 353 Type: cty.Set(cty.String), 354 Optional: true, 355 Description: "Assume role session tag keys to pass to any subsequent sessions.", 356 }, 357 // 358 // NOT SUPPORTED by `aws-sdk-go-base/v1` 359 // Cannot be added yet. 360 // 361 // "source_identity": stringAttribute{ 362 // configschema.Attribute{ 363 // Type: cty.String, 364 // Optional: true, 365 // Description: "Source identity specified by the principal assuming the role.", 366 // ValidateFunc: validAssumeRoleSourceIdentity, 367 // }, 368 // }, 369 }, 370 }, 371 }, 372 "assume_role_with_web_identity": { 373 Optional: true, 374 NestedType: &configschema.Object{ 375 Nesting: configschema.NestingSingle, 376 Attributes: map[string]*configschema.Attribute{ 377 "role_arn": { 378 Type: cty.String, 379 Optional: true, 380 Description: "The Amazon Resource Name (ARN) role to assume.", 381 }, 382 "web_identity_token": { 383 Type: cty.String, 384 Optional: true, 385 Sensitive: true, 386 Description: "The OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.", 387 }, 388 "web_identity_token_file": { 389 Type: cty.String, 390 Optional: true, 391 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.", 392 }, 393 "session_name": { 394 Type: cty.String, 395 Optional: true, 396 Description: "The name applied to this assume-role session.", 397 }, 398 "policy": { 399 Type: cty.String, 400 Optional: true, 401 Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", 402 }, 403 "policy_arns": { 404 Type: cty.Set(cty.String), 405 Optional: true, 406 Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", 407 }, 408 "duration": { 409 Type: cty.String, 410 Optional: true, 411 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.", 412 }, 413 }, 414 }, 415 }, 416 "forbidden_account_ids": { 417 Type: cty.Set(cty.String), 418 Optional: true, 419 Description: "List of forbidden AWS account IDs.", 420 }, 421 "allowed_account_ids": { 422 Type: cty.Set(cty.String), 423 Optional: true, 424 Description: "List of allowed AWS account IDs.", 425 }, 426 "http_proxy": { 427 Type: cty.String, 428 Optional: true, 429 Description: "The address of an HTTP proxy to use when accessing the AWS API.", 430 }, 431 "https_proxy": { 432 Type: cty.String, 433 Optional: true, 434 Description: "The address of an HTTPS proxy to use when accessing the AWS API.", 435 }, 436 "no_proxy": { 437 Type: cty.String, 438 Optional: true, 439 Description: `Comma-separated values which specify hosts that should be excluded from proxying. 440 See details: https://cs.opensource.google/go/x/net/+/refs/tags/v0.17.0:http/httpproxy/proxy.go;l=38-50.`, 441 }, 442 "insecure": { 443 Type: cty.Bool, 444 Optional: true, 445 Description: "Explicitly allow the backend to perform \"insecure\" SSL requests.", 446 }, 447 "use_dualstack_endpoint": { 448 Type: cty.Bool, 449 Optional: true, 450 Description: "Resolve an endpoint with DualStack capability.", 451 }, 452 "use_fips_endpoint": { 453 Type: cty.Bool, 454 Optional: true, 455 Description: "Resolve an endpoint with FIPS capability.", 456 }, 457 "skip_s3_checksum": { 458 Type: cty.Bool, 459 Optional: true, 460 Description: "Do not include checksum when uploading S3 Objects. Useful for some S3-Compatible APIs as some of them do not support checksum checks.", 461 }, 462 }, 463 } 464 } 465 466 // PrepareConfig checks the validity of the values in the given 467 // configuration, and inserts any missing defaults, assuming that its 468 // structure has already been validated per the schema returned by 469 // ConfigSchema. 470 func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { 471 var diags tfdiags.Diagnostics 472 if obj.IsNull() { 473 return obj, diags 474 } 475 476 if val := obj.GetAttr("bucket"); val.IsNull() || val.AsString() == "" { 477 diags = diags.Append(tfdiags.AttributeValue( 478 tfdiags.Error, 479 "Invalid bucket value", 480 `The "bucket" attribute value must not be empty.`, 481 cty.Path{cty.GetAttrStep{Name: "bucket"}}, 482 )) 483 } 484 485 if val := obj.GetAttr("key"); val.IsNull() || val.AsString() == "" { 486 diags = diags.Append(tfdiags.AttributeValue( 487 tfdiags.Error, 488 "Invalid key value", 489 `The "key" attribute value must not be empty.`, 490 cty.Path{cty.GetAttrStep{Name: "key"}}, 491 )) 492 } else if strings.HasPrefix(val.AsString(), "/") || strings.HasSuffix(val.AsString(), "/") { 493 // S3 will strip leading slashes from an object, so while this will 494 // technically be accepted by S3, it will break our workspace hierarchy. 495 // S3 will recognize objects with a trailing slash as a directory 496 // so they should not be valid keys 497 diags = diags.Append(tfdiags.AttributeValue( 498 tfdiags.Error, 499 "Invalid key value", 500 `The "key" attribute value must not start or end with with "/".`, 501 cty.Path{cty.GetAttrStep{Name: "key"}}, 502 )) 503 } 504 505 if val := obj.GetAttr("region"); val.IsNull() || val.AsString() == "" { 506 if os.Getenv("AWS_REGION") == "" && os.Getenv("AWS_DEFAULT_REGION") == "" { 507 diags = diags.Append(tfdiags.AttributeValue( 508 tfdiags.Error, 509 "Missing region value", 510 `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`, 511 cty.Path{cty.GetAttrStep{Name: "region"}}, 512 )) 513 } 514 } 515 516 if val := obj.GetAttr("kms_key_id"); !val.IsNull() && val.AsString() != "" { 517 if val := obj.GetAttr("sse_customer_key"); !val.IsNull() && val.AsString() != "" { 518 diags = diags.Append(tfdiags.AttributeValue( 519 tfdiags.Error, 520 "Invalid encryption configuration", 521 encryptionKeyConflictError, 522 cty.Path{}, 523 )) 524 } else if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" { 525 diags = diags.Append(tfdiags.AttributeValue( 526 tfdiags.Error, 527 "Invalid encryption configuration", 528 encryptionKeyConflictEnvVarError, 529 cty.Path{}, 530 )) 531 } 532 533 diags = diags.Append(validateKMSKey(cty.Path{cty.GetAttrStep{Name: "kms_key_id"}}, val.AsString())) 534 } 535 536 if val := obj.GetAttr("workspace_key_prefix"); !val.IsNull() { 537 if v := val.AsString(); strings.HasPrefix(v, "/") || strings.HasSuffix(v, "/") { 538 diags = diags.Append(tfdiags.AttributeValue( 539 tfdiags.Error, 540 "Invalid workspace_key_prefix value", 541 `The "workspace_key_prefix" attribute value must not start with "/".`, 542 cty.Path{cty.GetAttrStep{Name: "workspace_key_prefix"}}, 543 )) 544 } 545 } 546 547 validateAttributesConflict( 548 cty.GetAttrPath("shared_credentials_file"), 549 cty.GetAttrPath("shared_credentials_files"), 550 )(obj, cty.Path{}, &diags) 551 552 attrPath := cty.GetAttrPath("shared_credentials_file") 553 if val := obj.GetAttr("shared_credentials_file"); !val.IsNull() { 554 detail := fmt.Sprintf( 555 `Parameter "%s" is deprecated. Use "%s" instead.`, 556 pathString(attrPath), 557 pathString(cty.GetAttrPath("shared_credentials_files"))) 558 559 diags = diags.Append(attributeWarningDiag( 560 "Deprecated Parameter", 561 detail, 562 attrPath)) 563 } 564 565 if val := obj.GetAttr("force_path_style"); !val.IsNull() { 566 attrPath := cty.GetAttrPath("force_path_style") 567 detail := fmt.Sprintf( 568 `Parameter "%s" is deprecated. Use "%s" instead.`, 569 pathString(attrPath), 570 pathString(cty.GetAttrPath("use_path_style"))) 571 572 diags = diags.Append(attributeWarningDiag( 573 "Deprecated Parameter", 574 detail, 575 attrPath)) 576 } 577 578 if val := obj.GetAttr("use_legacy_workflow"); !val.IsNull() { 579 attrPath := cty.GetAttrPath("use_legacy_workflow") 580 detail := fmt.Sprintf( 581 `Parameter "%s" is deprecated and will be removed in an upcoming minor version.`, 582 pathString(attrPath)) 583 584 diags = diags.Append(attributeWarningDiag( 585 "Deprecated Parameter", 586 detail, 587 attrPath)) 588 } 589 590 validateAttributesConflict( 591 cty.GetAttrPath("force_path_style"), 592 cty.GetAttrPath("use_path_style"), 593 )(obj, cty.Path{}, &diags) 594 595 var assumeRoleDeprecatedFields = map[string]string{ 596 "role_arn": "assume_role.role_arn", 597 "session_name": "assume_role.session_name", 598 "external_id": "assume_role.external_id", 599 "assume_role_duration_seconds": "assume_role.duration", 600 "assume_role_policy": "assume_role.policy", 601 "assume_role_policy_arns": "assume_role.policy_arns", 602 "assume_role_tags": "assume_role.tags", 603 "assume_role_transitive_tag_keys": "assume_role.transitive_tag_keys", 604 } 605 606 if val := obj.GetAttr("assume_role"); !val.IsNull() { 607 diags = diags.Append(validateNestedAssumeRole(val, cty.Path{cty.GetAttrStep{Name: "assume_role"}})) 608 609 if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 { 610 diags = diags.Append(tfdiags.WholeContainingBody( 611 tfdiags.Error, 612 "Conflicting Parameters", 613 `The following deprecated parameters conflict with the parameter "assume_role". Replace them as follows:`+"\n"+ 614 formatDeprecated(defined), 615 )) 616 } 617 } else { 618 if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 { 619 diags = diags.Append(tfdiags.WholeContainingBody( 620 tfdiags.Warning, 621 "Deprecated Parameters", 622 `The following parameters have been deprecated. Replace them as follows:`+"\n"+ 623 formatDeprecated(defined), 624 )) 625 } 626 } 627 628 if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { 629 diags = diags.Append(validateAssumeRoleWithWebIdentity(val, cty.GetAttrPath("assume_role_with_web_identity"))) 630 } 631 632 validateAttributesConflict( 633 cty.GetAttrPath("allowed_account_ids"), 634 cty.GetAttrPath("forbidden_account_ids"), 635 )(obj, cty.Path{}, &diags) 636 637 if val := obj.GetAttr("retry_mode"); !val.IsNull() { 638 s := val.AsString() 639 if _, err := aws.ParseRetryMode(s); err != nil { 640 diags = diags.Append(tfdiags.AttributeValue( 641 tfdiags.Error, 642 "Invalid retry mode", 643 fmt.Sprintf("Valid values are %q and %q.", aws.RetryModeStandard, aws.RetryModeAdaptive), 644 cty.Path{cty.GetAttrStep{Name: "retry_mode"}}, 645 )) 646 } 647 } 648 649 for _, endpoint := range customEndpoints { 650 endpoint.Validate(obj, &diags) 651 } 652 653 return obj, diags 654 } 655 656 // Configure uses the provided configuration to set configuration fields 657 // within the backend. 658 // 659 // The given configuration is assumed to have already been validated 660 // against the schema returned by ConfigSchema and passed validation 661 // via PrepareConfig. 662 func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { 663 var diags tfdiags.Diagnostics 664 if obj.IsNull() { 665 return diags 666 } 667 668 var region string 669 if v, ok := stringAttrOk(obj, "region"); ok { 670 region = v 671 } 672 673 if region != "" && !boolAttr(obj, "skip_region_validation") { 674 if err := awsbaseValidation.SupportedRegion(region); err != nil { 675 diags = diags.Append(tfdiags.AttributeValue( 676 tfdiags.Error, 677 "Invalid region value", 678 err.Error(), 679 cty.Path{cty.GetAttrStep{Name: "region"}}, 680 )) 681 return diags 682 } 683 } 684 685 b.bucketName = stringAttr(obj, "bucket") 686 b.keyName = stringAttr(obj, "key") 687 b.acl = stringAttr(obj, "acl") 688 b.workspaceKeyPrefix = stringAttrDefault(obj, "workspace_key_prefix", "env:") 689 b.serverSideEncryption = boolAttr(obj, "encrypt") 690 b.kmsKeyID = stringAttr(obj, "kms_key_id") 691 b.ddbTable = stringAttr(obj, "dynamodb_table") 692 b.skipS3Checksum = boolAttr(obj, "skip_s3_checksum") 693 694 if customerKey, ok := stringAttrOk(obj, "sse_customer_key"); ok { 695 if len(customerKey) != 44 { 696 diags = diags.Append(tfdiags.AttributeValue( 697 tfdiags.Error, 698 "Invalid sse_customer_key value", 699 "sse_customer_key must be 44 characters in length", 700 cty.Path{cty.GetAttrStep{Name: "sse_customer_key"}}, 701 )) 702 } else { 703 var err error 704 if b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKey); err != nil { 705 diags = diags.Append(tfdiags.AttributeValue( 706 tfdiags.Error, 707 "Invalid sse_customer_key value", 708 fmt.Sprintf("sse_customer_key must be base64 encoded: %s", err), 709 cty.Path{cty.GetAttrStep{Name: "sse_customer_key"}}, 710 )) 711 } 712 } 713 } else if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" { 714 if len(customerKey) != 44 { 715 diags = diags.Append(tfdiags.Sourceless( 716 tfdiags.Error, 717 "Invalid AWS_SSE_CUSTOMER_KEY value", 718 `The environment variable "AWS_SSE_CUSTOMER_KEY" must be 44 characters in length`, 719 )) 720 } else { 721 var err error 722 if b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKey); err != nil { 723 diags = diags.Append(tfdiags.Sourceless( 724 tfdiags.Error, 725 "Invalid AWS_SSE_CUSTOMER_KEY value", 726 fmt.Sprintf(`The environment variable "AWS_SSE_CUSTOMER_KEY" must be base64 encoded: %s`, err), 727 )) 728 } 729 } 730 } 731 732 ctx := context.TODO() 733 ctx, baselog := attachLoggerToContext(ctx) 734 735 cfg := &awsbase.Config{ 736 AccessKey: stringAttr(obj, "access_key"), 737 CallerDocumentationURL: "https://opentofu.org/docs/language/settings/backends/s3", 738 CallerName: "S3 Backend", 739 IamEndpoint: customEndpoints["iam"].String(obj), 740 MaxRetries: intAttrDefault(obj, "max_retries", 5), 741 Profile: stringAttr(obj, "profile"), 742 Region: stringAttr(obj, "region"), 743 SecretKey: stringAttr(obj, "secret_key"), 744 SkipCredsValidation: boolAttr(obj, "skip_credentials_validation"), 745 SkipRequestingAccountId: boolAttr(obj, "skip_requesting_account_id"), 746 StsEndpoint: customEndpoints["sts"].String(obj), 747 StsRegion: stringAttr(obj, "sts_region"), 748 Token: stringAttr(obj, "token"), 749 750 // Note: we don't need to read env variables explicitly because they are read implicitly by aws-sdk-base-go: 751 // see: https://github.com/hashicorp/aws-sdk-go-base/blob/v2.0.0-beta.41/internal/config/config.go#L133 752 // which relies on: https://cs.opensource.google/go/x/net/+/refs/tags/v0.18.0:http/httpproxy/proxy.go;l=89-96 753 HTTPProxy: aws.String(stringAttrDefaultEnvVar(obj, "http_proxy", "HTTP_PROXY")), 754 HTTPSProxy: aws.String(stringAttrDefaultEnvVar(obj, "https_proxy", "HTTPS_PROXY")), 755 NoProxy: stringAttrDefaultEnvVar(obj, "no_proxy", "NO_PROXY"), 756 Insecure: boolAttr(obj, "insecure"), 757 UseDualStackEndpoint: boolAttr(obj, "use_dualstack_endpoint"), 758 UseFIPSEndpoint: boolAttr(obj, "use_fips_endpoint"), 759 UserAgent: awsbase.UserAgentProducts{ 760 {Name: "APN", Version: "1.0"}, 761 {Name: httpclient.DefaultApplicationName, Version: version.String()}, 762 }, 763 CustomCABundle: stringAttrDefaultEnvVar(obj, "custom_ca_bundle", "AWS_CA_BUNDLE"), 764 EC2MetadataServiceEndpoint: stringAttrDefaultEnvVar(obj, "ec2_metadata_service_endpoint", "AWS_EC2_METADATA_SERVICE_ENDPOINT"), 765 EC2MetadataServiceEndpointMode: stringAttrDefaultEnvVar(obj, "ec2_metadata_service_endpoint_mode", "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE"), 766 Logger: baselog, 767 } 768 769 cfg.UseLegacyWorkflow = boolAttr(obj, "use_legacy_workflow") 770 771 if val, ok := boolAttrOk(obj, "skip_metadata_api_check"); ok { 772 if val { 773 cfg.EC2MetadataServiceEnableState = imds.ClientDisabled 774 } else { 775 cfg.EC2MetadataServiceEnableState = imds.ClientEnabled 776 } 777 } 778 779 if val, ok := stringAttrOk(obj, "shared_credentials_file"); ok { 780 cfg.SharedCredentialsFiles = []string{val} 781 } 782 783 if value := obj.GetAttr("assume_role"); !value.IsNull() { 784 cfg.AssumeRole = configureNestedAssumeRole(obj) 785 } else if value := obj.GetAttr("role_arn"); !value.IsNull() { 786 cfg.AssumeRole = configureAssumeRole(obj) 787 } 788 789 if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { 790 cfg.AssumeRoleWithWebIdentity = configureAssumeRoleWithWebIdentity(val) 791 } 792 793 if val, ok := stringSliceAttrDefaultEnvVarOk(obj, "shared_credentials_files", "AWS_SHARED_CREDENTIALS_FILE"); ok { 794 cfg.SharedCredentialsFiles = val 795 } 796 if val, ok := stringSliceAttrDefaultEnvVarOk(obj, "shared_config_files", "AWS_SHARED_CONFIG_FILE"); ok { 797 cfg.SharedConfigFiles = val 798 } 799 800 if val, ok := stringSliceAttrOk(obj, "allowed_account_ids"); ok { 801 cfg.AllowedAccountIds = val 802 } 803 804 if val, ok := stringSliceAttrOk(obj, "forbidden_account_ids"); ok { 805 cfg.ForbiddenAccountIds = val 806 } 807 808 if val, ok := stringAttrOk(obj, "retry_mode"); ok { 809 mode, err := aws.ParseRetryMode(val) 810 if err != nil { 811 panic(fmt.Sprintf("invalid retry mode %q: %s", val, err)) 812 } 813 cfg.RetryMode = mode 814 } 815 816 _, awsConfig, awsDiags := awsbase.GetAwsConfig(ctx, cfg) 817 818 for _, d := range awsDiags { 819 diags = diags.Append(tfdiags.Sourceless( 820 baseSeverityToTofuSeverity(d.Severity()), 821 d.Summary(), 822 d.Detail(), 823 )) 824 } 825 826 if d := verifyAllowedAccountID(ctx, awsConfig, cfg); len(d) != 0 { 827 diags = diags.Append(d) 828 } 829 830 if diags.HasErrors() { 831 return diags 832 } 833 834 b.awsConfig = awsConfig 835 836 b.dynClient = dynamodb.NewFromConfig(awsConfig, getDynamoDBConfig(obj)) 837 838 b.s3Client = s3.NewFromConfig(awsConfig, getS3Config(obj)) 839 840 return diags 841 } 842 843 func attachLoggerToContext(ctx context.Context) (context.Context, baselogging.HcLogger) { 844 ctx, baselog := baselogging.NewHcLogger(ctx, logging.HCLogger().Named("backend-s3")) 845 ctx = baselogging.RegisterLogger(ctx, baselog) 846 return ctx, baselog 847 } 848 849 func verifyAllowedAccountID(ctx context.Context, awsConfig aws.Config, cfg *awsbase.Config) tfdiags.Diagnostics { 850 if len(cfg.ForbiddenAccountIds) == 0 && len(cfg.AllowedAccountIds) == 0 { 851 return nil 852 } 853 854 var diags tfdiags.Diagnostics 855 accountID, _, awsDiags := awsbase.GetAwsAccountIDAndPartition(ctx, awsConfig, cfg) 856 for _, d := range awsDiags { 857 diags = diags.Append(tfdiags.Sourceless( 858 baseSeverityToTofuSeverity(d.Severity()), 859 fmt.Sprintf("Retrieving AWS account details: %s", d.Summary()), 860 d.Detail(), 861 )) 862 } 863 864 err := cfg.VerifyAccountIDAllowed(accountID) 865 if err != nil { 866 diags = diags.Append(tfdiags.Sourceless( 867 tfdiags.Error, 868 "Invalid account ID", 869 err.Error(), 870 )) 871 } 872 return diags 873 } 874 875 func getDynamoDBConfig(obj cty.Value) func(options *dynamodb.Options) { 876 return func(options *dynamodb.Options) { 877 if v, ok := customEndpoints["dynamodb"].StringOk(obj); ok { 878 options.BaseEndpoint = aws.String(v) 879 } 880 } 881 } 882 883 func getS3Config(obj cty.Value) func(options *s3.Options) { 884 return func(options *s3.Options) { 885 if v, ok := customEndpoints["s3"].StringOk(obj); ok { 886 options.BaseEndpoint = aws.String(v) 887 } 888 if v, ok := boolAttrOk(obj, "force_path_style"); ok { 889 options.UsePathStyle = v 890 } 891 if v, ok := boolAttrOk(obj, "use_path_style"); ok { 892 options.UsePathStyle = v 893 } 894 } 895 } 896 897 func configureNestedAssumeRole(obj cty.Value) *awsbase.AssumeRole { 898 assumeRole := awsbase.AssumeRole{} 899 900 obj = obj.GetAttr("assume_role") 901 if val, ok := stringAttrOk(obj, "role_arn"); ok { 902 assumeRole.RoleARN = val 903 } 904 if val, ok := stringAttrOk(obj, "duration"); ok { 905 dur, err := time.ParseDuration(val) 906 if err != nil { 907 // This should never happen because the schema should have 908 // already validated the duration. 909 panic(fmt.Sprintf("invalid duration %q: %s", val, err)) 910 } 911 912 assumeRole.Duration = dur 913 } 914 if val, ok := stringAttrOk(obj, "external_id"); ok { 915 assumeRole.ExternalID = val 916 } 917 918 if val, ok := stringAttrOk(obj, "policy"); ok { 919 assumeRole.Policy = strings.TrimSpace(val) 920 } 921 if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok { 922 assumeRole.PolicyARNs = val 923 } 924 if val, ok := stringAttrOk(obj, "session_name"); ok { 925 assumeRole.SessionName = val 926 } 927 if val, ok := stringMapAttrOk(obj, "tags"); ok { 928 assumeRole.Tags = val 929 } 930 if val, ok := stringSliceAttrOk(obj, "transitive_tag_keys"); ok { 931 assumeRole.TransitiveTagKeys = val 932 } 933 934 return &assumeRole 935 } 936 937 func configureAssumeRole(obj cty.Value) *awsbase.AssumeRole { 938 assumeRole := awsbase.AssumeRole{} 939 940 assumeRole.RoleARN = stringAttr(obj, "role_arn") 941 assumeRole.Duration = time.Duration(int64(intAttr(obj, "assume_role_duration_seconds")) * int64(time.Second)) 942 assumeRole.ExternalID = stringAttr(obj, "external_id") 943 assumeRole.Policy = stringAttr(obj, "assume_role_policy") 944 assumeRole.SessionName = stringAttr(obj, "session_name") 945 946 if val, ok := stringSliceAttrOk(obj, "assume_role_policy_arns"); ok { 947 assumeRole.PolicyARNs = val 948 } 949 if val, ok := stringMapAttrOk(obj, "assume_role_tags"); ok { 950 assumeRole.Tags = val 951 } 952 if val, ok := stringSliceAttrOk(obj, "assume_role_transitive_tag_keys"); ok { 953 assumeRole.TransitiveTagKeys = val 954 } 955 956 return &assumeRole 957 } 958 959 func configureAssumeRoleWithWebIdentity(obj cty.Value) *awsbase.AssumeRoleWithWebIdentity { 960 cfg := &awsbase.AssumeRoleWithWebIdentity{ 961 RoleARN: stringAttrDefaultEnvVar(obj, "role_arn", "AWS_ROLE_ARN"), 962 Policy: stringAttr(obj, "policy"), 963 PolicyARNs: stringSliceAttr(obj, "policy_arns"), 964 SessionName: stringAttrDefaultEnvVar(obj, "session_name", "AWS_ROLE_SESSION_NAME"), 965 WebIdentityToken: stringAttrDefaultEnvVar(obj, "web_identity_token", "AWS_WEB_IDENTITY_TOKEN"), 966 WebIdentityTokenFile: stringAttrDefaultEnvVar(obj, "web_identity_token_file", "AWS_WEB_IDENTITY_TOKEN_FILE"), 967 } 968 if val, ok := stringAttrOk(obj, "duration"); ok { 969 d, err := time.ParseDuration(val) 970 if err != nil { 971 // This should never happen because the schema should have 972 // already validated the duration. 973 panic(fmt.Sprintf("invalid duration %q: %s", val, err)) 974 } 975 cfg.Duration = d 976 } 977 return cfg 978 } 979 980 func stringValue(val cty.Value) string { 981 v, _ := stringValueOk(val) 982 return v 983 } 984 985 func stringValueOk(val cty.Value) (string, bool) { 986 if val.IsNull() { 987 return "", false 988 } else { 989 return val.AsString(), true 990 } 991 } 992 993 func stringAttr(obj cty.Value, name string) string { 994 return stringValue(obj.GetAttr(name)) 995 } 996 997 func stringAttrOk(obj cty.Value, name string) (string, bool) { 998 return stringValueOk(obj.GetAttr(name)) 999 } 1000 1001 func stringAttrDefault(obj cty.Value, name, def string) string { 1002 if v, ok := stringAttrOk(obj, name); !ok { 1003 return def 1004 } else { 1005 return v 1006 } 1007 } 1008 1009 func stringSliceValue(val cty.Value) []string { 1010 v, _ := stringSliceValueOk(val) 1011 return v 1012 } 1013 1014 func stringSliceValueOk(val cty.Value) ([]string, bool) { 1015 if val.IsNull() { 1016 return nil, false 1017 } 1018 1019 var v []string 1020 if err := gocty.FromCtyValue(val, &v); err != nil { 1021 return nil, false 1022 } 1023 return v, true 1024 } 1025 1026 func stringSliceAttr(obj cty.Value, name string) []string { 1027 return stringSliceValue(obj.GetAttr(name)) 1028 } 1029 1030 func stringSliceAttrOk(obj cty.Value, name string) ([]string, bool) { 1031 return stringSliceValueOk(obj.GetAttr(name)) 1032 } 1033 1034 func stringSliceAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) ([]string, bool) { 1035 if v, ok := stringSliceAttrOk(obj, name); !ok { 1036 for _, envvar := range envvars { 1037 if ev := os.Getenv(envvar); ev != "" { 1038 return []string{ev}, true 1039 } 1040 } 1041 return nil, false 1042 } else { 1043 return v, true 1044 } 1045 } 1046 1047 func stringAttrDefaultEnvVar(obj cty.Value, name string, envvars ...string) string { 1048 if v, ok := stringAttrDefaultEnvVarOk(obj, name, envvars...); !ok { 1049 return "" 1050 } else { 1051 return v 1052 } 1053 } 1054 1055 func stringAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) (string, bool) { 1056 if v, ok := stringAttrOk(obj, name); !ok { 1057 for _, envvar := range envvars { 1058 if v := os.Getenv(envvar); v != "" { 1059 return v, true 1060 } 1061 } 1062 return "", false 1063 } else { 1064 return v, true 1065 } 1066 } 1067 1068 func boolAttr(obj cty.Value, name string) bool { 1069 v, _ := boolAttrOk(obj, name) 1070 return v 1071 } 1072 1073 func boolAttrOk(obj cty.Value, name string) (bool, bool) { 1074 if val := obj.GetAttr(name); val.IsNull() { 1075 return false, false 1076 } else { 1077 return val.True(), true 1078 } 1079 } 1080 1081 func intAttr(obj cty.Value, name string) int { 1082 v, _ := intAttrOk(obj, name) 1083 return v 1084 } 1085 1086 func intAttrOk(obj cty.Value, name string) (int, bool) { 1087 if val := obj.GetAttr(name); val.IsNull() { 1088 return 0, false 1089 } else { 1090 var v int 1091 if err := gocty.FromCtyValue(val, &v); err != nil { 1092 return 0, false 1093 } 1094 return v, true 1095 } 1096 } 1097 1098 func intAttrDefault(obj cty.Value, name string, def int) int { 1099 if v, ok := intAttrOk(obj, name); !ok { 1100 return def 1101 } else { 1102 return v 1103 } 1104 } 1105 1106 func stringMapValueOk(val cty.Value) (map[string]string, bool) { 1107 var m map[string]string 1108 err := gocty.FromCtyValue(val, &m) 1109 if err != nil { 1110 return nil, false 1111 } 1112 return m, true 1113 } 1114 1115 func stringMapAttrOk(obj cty.Value, name string) (map[string]string, bool) { 1116 return stringMapValueOk(obj.GetAttr(name)) 1117 } 1118 1119 func customEndpointAttrDefaultEnvVarOk(obj cty.Value, endpointsKey, deprecatedKey string, envvars ...string) (string, bool) { 1120 if val := obj.GetAttr("endpoints"); !val.IsNull() { 1121 if v, ok := stringAttrDefaultEnvVarOk(val, endpointsKey, envvars...); ok { 1122 return v, true 1123 } 1124 } 1125 return stringAttrDefaultEnvVarOk(obj, deprecatedKey, envvars...) 1126 } 1127 1128 func pathString(path cty.Path) string { 1129 var buf strings.Builder 1130 for i, step := range path { 1131 switch x := step.(type) { 1132 case cty.GetAttrStep: 1133 if i != 0 { 1134 buf.WriteString(".") 1135 } 1136 buf.WriteString(x.Name) 1137 case cty.IndexStep: 1138 val := x.Key 1139 typ := val.Type() 1140 var s string 1141 switch { 1142 case typ == cty.String: 1143 s = val.AsString() 1144 case typ == cty.Number: 1145 num := val.AsBigFloat() 1146 if num.IsInt() { 1147 s = num.Text('f', -1) 1148 } else { 1149 s = num.String() 1150 } 1151 default: 1152 s = fmt.Sprintf("<unexpected index: %s>", typ.FriendlyName()) 1153 } 1154 buf.WriteString(fmt.Sprintf("[%s]", s)) 1155 default: 1156 if i != 0 { 1157 buf.WriteString(".") 1158 } 1159 buf.WriteString(fmt.Sprintf("<unexpected step: %[1]T %[1]v>", x)) 1160 } 1161 } 1162 return buf.String() 1163 } 1164 1165 func findDeprecatedFields(obj cty.Value, attrs map[string]string) map[string]string { 1166 defined := make(map[string]string) 1167 for attr, v := range attrs { 1168 if val := obj.GetAttr(attr); !val.IsNull() { 1169 defined[attr] = v 1170 } 1171 } 1172 return defined 1173 } 1174 1175 func formatDeprecated(attrs map[string]string) string { 1176 var maxLen int 1177 var buf strings.Builder 1178 1179 names := make([]string, 0, len(attrs)) 1180 for deprecated, replacement := range attrs { 1181 names = append(names, deprecated) 1182 if l := len(deprecated); l > maxLen { 1183 maxLen = l 1184 } 1185 1186 fmt.Fprintf(&buf, " * %-[1]*[2]s -> %s\n", maxLen, deprecated, replacement) 1187 } 1188 1189 sort.Strings(names) 1190 1191 return buf.String() 1192 } 1193 1194 const encryptionKeyConflictError = `Only one of "kms_key_id" and "sse_customer_key" can be set. 1195 1196 The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS) 1197 while "sse_customer_key" is used for encryption with customer-managed keys (SSE-C). 1198 Please choose one or the other.` 1199 1200 const encryptionKeyConflictEnvVarError = `Only one of "kms_key_id" and the environment variable "AWS_SSE_CUSTOMER_KEY" can be set. 1201 1202 The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS) 1203 while "AWS_SSE_CUSTOMER_KEY" is used for encryption with customer-managed keys (SSE-C). 1204 Please choose one or the other.` 1205 1206 type customEndpoint struct { 1207 Paths []cty.Path 1208 EnvVars []string 1209 } 1210 1211 func (e customEndpoint) Validate(obj cty.Value, diags *tfdiags.Diagnostics) { 1212 validateAttributesConflict(e.Paths...)(obj, cty.Path{}, diags) 1213 } 1214 1215 func (e customEndpoint) String(obj cty.Value) string { 1216 v, _ := e.StringOk(obj) 1217 return v 1218 } 1219 1220 func includeProtoIfNessesary(endpoint string) string { 1221 if matched, _ := regexp.MatchString("[a-z]*://.*", endpoint); !matched { 1222 log.Printf("[DEBUG] Adding https:// prefix to endpoint '%s'", endpoint) 1223 endpoint = fmt.Sprintf("https://%s", endpoint) 1224 } 1225 return endpoint 1226 } 1227 1228 func (e customEndpoint) StringOk(obj cty.Value) (string, bool) { 1229 for _, path := range e.Paths { 1230 val, err := path.Apply(obj) 1231 if err != nil { 1232 continue 1233 } 1234 if s, ok := stringValueOk(val); ok { 1235 return includeProtoIfNessesary(s), true 1236 } 1237 } 1238 for _, envVar := range e.EnvVars { 1239 if v := os.Getenv(envVar); v != "" { 1240 return includeProtoIfNessesary(v), true 1241 } 1242 } 1243 return "", false 1244 } 1245 1246 var customEndpoints = map[string]customEndpoint{ 1247 "s3": { 1248 Paths: []cty.Path{ 1249 cty.GetAttrPath("endpoints").GetAttr("s3"), 1250 cty.GetAttrPath("endpoint"), 1251 }, 1252 EnvVars: []string{ 1253 "AWS_ENDPOINT_URL_S3", 1254 "AWS_S3_ENDPOINT", 1255 }, 1256 }, 1257 "iam": { 1258 Paths: []cty.Path{ 1259 cty.GetAttrPath("endpoints").GetAttr("iam"), 1260 cty.GetAttrPath("iam_endpoint"), 1261 }, 1262 EnvVars: []string{ 1263 "AWS_ENDPOINT_URL_IAM", 1264 "AWS_IAM_ENDPOINT", 1265 }, 1266 }, 1267 "sts": { 1268 Paths: []cty.Path{ 1269 cty.GetAttrPath("endpoints").GetAttr("sts"), 1270 cty.GetAttrPath("sts_endpoint"), 1271 }, 1272 EnvVars: []string{ 1273 "AWS_ENDPOINT_URL_STS", 1274 "AWS_STS_ENDPOINT", 1275 }, 1276 }, 1277 "dynamodb": { 1278 Paths: []cty.Path{ 1279 cty.GetAttrPath("endpoints").GetAttr("dynamodb"), 1280 cty.GetAttrPath("dynamodb_endpoint"), 1281 }, 1282 EnvVars: []string{ 1283 "AWS_ENDPOINT_URL_DYNAMODB", 1284 "AWS_DYNAMODB_ENDPOINT", 1285 }, 1286 }, 1287 }