mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Backend/s3: Add support for assume_role_with_web_identity
block (#759)
Signed-off-by: Marcin Białoń <mbialon@spacelift.io>
This commit is contained in:
parent
a6a54c3777
commit
4d38f26bf7
@ -53,6 +53,7 @@ S3 BACKEND:
|
||||
* 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))
|
||||
* Adds support for the `assume_role_with_web_identity` block. ([#689](https://github.com/opentofu/opentofu/issues/689))
|
||||
* Adds support for account whitelisting using the `forbidden_account_ids` and `allowed_account_ids` arguments. ([#699](https://github.com/opentofu/opentofu/issues/699))
|
||||
* Adds the `custom_ca_bundle` argument. ([#689](https://github.com/opentofu/opentofu/issues/689))
|
||||
|
||||
|
@ -180,54 +180,46 @@ func (b *Backend) ConfigSchema(context.Context) *configschema.Block {
|
||||
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.",
|
||||
},
|
||||
|
||||
"max_retries": {
|
||||
Type: cty.Number,
|
||||
Optional: true,
|
||||
@ -302,6 +294,49 @@ func (b *Backend) ConfigSchema(context.Context) *configschema.Block {
|
||||
},
|
||||
},
|
||||
},
|
||||
"assume_role_with_web_identity": {
|
||||
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,
|
||||
@ -448,6 +483,10 @@ func (b *Backend) PrepareConfig(ctx context.Context, obj cty.Value) (cty.Value,
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
@ -583,6 +622,10 @@ func (b *Backend) Configure(ctx context.Context, obj cty.Value) tfdiags.Diagnost
|
||||
cfg.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
|
||||
}
|
||||
@ -732,6 +775,27 @@ func configureAssumeRole(obj cty.Value) *awsbase.AssumeRole {
|
||||
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
|
||||
@ -761,6 +825,11 @@ func stringAttrDefault(obj cty.Value, name, def string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func stringSliceValue(val cty.Value) []string {
|
||||
v, _ := stringSliceValueOk(val)
|
||||
return v
|
||||
}
|
||||
|
||||
func stringSliceValueOk(val cty.Value) ([]string, bool) {
|
||||
if val.IsNull() {
|
||||
return nil, false
|
||||
@ -773,6 +842,10 @@ func stringSliceValueOk(val cty.Value) ([]string, bool) {
|
||||
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))
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
@ -1617,6 +1618,290 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendConfig_Authentication_AssumeRoleWithWebIdentity(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
config map[string]any
|
||||
SetConfig bool
|
||||
ExpandEnvVars bool
|
||||
EnvironmentVariables map[string]string
|
||||
SetTokenFileEnvironmentVariable bool
|
||||
SharedConfigurationFile string
|
||||
SetSharedConfigurationFile bool
|
||||
ExpectedCredentialsValue aws.Credentials
|
||||
ValidateDiags diagsValidator
|
||||
MockStsEndpoints []*servicemocks.MockEndpoint
|
||||
}{
|
||||
"config with inline token": {
|
||||
config: map[string]any{
|
||||
"assume_role_with_web_identity": map[string]any{
|
||||
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
"web_identity_token": servicemocks.MockWebIdentityToken,
|
||||
},
|
||||
},
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
|
||||
},
|
||||
},
|
||||
|
||||
"config with token file": {
|
||||
config: map[string]any{
|
||||
"assume_role_with_web_identity": map[string]any{
|
||||
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
},
|
||||
},
|
||||
SetConfig: true,
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
|
||||
},
|
||||
},
|
||||
|
||||
"config with expanded path": {
|
||||
config: map[string]any{
|
||||
"assume_role_with_web_identity": map[string]any{
|
||||
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
},
|
||||
},
|
||||
SetConfig: true,
|
||||
ExpandEnvVars: true,
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
|
||||
},
|
||||
},
|
||||
|
||||
"envvar": {
|
||||
config: map[string]any{},
|
||||
EnvironmentVariables: map[string]string{
|
||||
"AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
},
|
||||
SetTokenFileEnvironmentVariable: true,
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
|
||||
},
|
||||
},
|
||||
|
||||
"shared configuration file": {
|
||||
config: map[string]any{},
|
||||
SharedConfigurationFile: fmt.Sprintf(`
|
||||
[default]
|
||||
role_arn = %[1]s
|
||||
role_session_name = %[2]s
|
||||
`, servicemocks.MockStsAssumeRoleWithWebIdentityArn, servicemocks.MockStsAssumeRoleWithWebIdentitySessionName),
|
||||
SetSharedConfigurationFile: true,
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
|
||||
},
|
||||
},
|
||||
|
||||
"config overrides envvar": {
|
||||
config: map[string]any{
|
||||
"assume_role_with_web_identity": map[string]any{
|
||||
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
"web_identity_token": servicemocks.MockWebIdentityToken,
|
||||
},
|
||||
},
|
||||
EnvironmentVariables: map[string]string{
|
||||
"AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn,
|
||||
"AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName,
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE": "no-such-file",
|
||||
},
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
|
||||
},
|
||||
},
|
||||
|
||||
"envvar overrides shared configuration": {
|
||||
config: map[string]any{},
|
||||
EnvironmentVariables: map[string]string{
|
||||
"AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
},
|
||||
SetTokenFileEnvironmentVariable: true,
|
||||
SharedConfigurationFile: fmt.Sprintf(`
|
||||
[default]
|
||||
role_arn = %[1]s
|
||||
role_session_name = %[2]s
|
||||
web_identity_token_file = no-such-file
|
||||
`, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName),
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
|
||||
},
|
||||
},
|
||||
|
||||
"config overrides shared configuration": {
|
||||
config: map[string]any{
|
||||
"assume_role_with_web_identity": map[string]any{
|
||||
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
"web_identity_token": servicemocks.MockWebIdentityToken,
|
||||
},
|
||||
},
|
||||
SharedConfigurationFile: fmt.Sprintf(`
|
||||
[default]
|
||||
role_arn = %[1]s
|
||||
role_session_name = %[2]s
|
||||
web_identity_token_file = no-such-file
|
||||
`, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName),
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
|
||||
},
|
||||
},
|
||||
|
||||
"with duration": {
|
||||
config: map[string]any{
|
||||
"assume_role_with_web_identity": map[string]any{
|
||||
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
"web_identity_token": servicemocks.MockWebIdentityToken,
|
||||
"duration": "1h",
|
||||
},
|
||||
},
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidWithOptions(map[string]string{"DurationSeconds": "3600"}),
|
||||
},
|
||||
},
|
||||
|
||||
"with policy": {
|
||||
config: map[string]any{
|
||||
"assume_role_with_web_identity": map[string]any{
|
||||
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
|
||||
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
|
||||
"web_identity_token": servicemocks.MockWebIdentityToken,
|
||||
"policy": "{}",
|
||||
},
|
||||
},
|
||||
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
|
||||
MockStsEndpoints: []*servicemocks.MockEndpoint{
|
||||
servicemocks.MockStsAssumeRoleWithWebIdentityValidWithOptions(map[string]string{"Policy": "{}"}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for k, v := range tc.EnvironmentVariables {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints)
|
||||
defer ts.Close()
|
||||
|
||||
tc.config["sts_endpoint"] = ts.URL
|
||||
|
||||
tempdir, err := os.MkdirTemp("", "temp")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating temp dir: %s", err)
|
||||
}
|
||||
defer os.Remove(tempdir)
|
||||
os.Setenv("TMPDIR", tempdir)
|
||||
|
||||
tokenFile, err := os.CreateTemp("", "aws-sdk-go-base-web-identity-token-file")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating temporary web identity token file: %s", err)
|
||||
}
|
||||
tokenFileName := tokenFile.Name()
|
||||
|
||||
defer os.Remove(tokenFileName)
|
||||
|
||||
err = os.WriteFile(tokenFileName, []byte(servicemocks.MockWebIdentityToken), 0600)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error writing web identity token file: %s", err)
|
||||
}
|
||||
|
||||
if tc.ExpandEnvVars {
|
||||
tmpdir := os.Getenv("TMPDIR")
|
||||
rel, err := filepath.Rel(tmpdir, tokenFileName)
|
||||
if err != nil {
|
||||
t.Fatalf("error making path relative: %s", err)
|
||||
}
|
||||
t.Logf("relative: %s", rel)
|
||||
tokenFileName = filepath.Join("$TMPDIR", rel)
|
||||
t.Logf("env tempfile: %s", tokenFileName)
|
||||
}
|
||||
|
||||
if tc.SetConfig {
|
||||
ar := tc.config["assume_role_with_web_identity"].(map[string]any)
|
||||
ar["web_identity_token_file"] = tokenFileName
|
||||
}
|
||||
|
||||
if tc.SetTokenFileEnvironmentVariable {
|
||||
os.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", tokenFileName)
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
if tc.SetSharedConfigurationFile {
|
||||
tc.SharedConfigurationFile += fmt.Sprintf("web_identity_token_file = %s\n", tokenFileName)
|
||||
}
|
||||
|
||||
err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error writing shared configuration file: %s", err)
|
||||
}
|
||||
|
||||
tc.config["shared_config_files"] = []any{file.Name()}
|
||||
}
|
||||
|
||||
tc.config["skip_credentials_validation"] = true
|
||||
|
||||
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 {
|
||||
config map[string]any
|
||||
|
@ -78,77 +78,50 @@ func validateNestedAssumeRole(obj cty.Value, objPath cty.Path) tfdiags.Diagnosti
|
||||
}
|
||||
|
||||
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,
|
||||
))
|
||||
}
|
||||
}
|
||||
validateDuration(val, 15*time.Minute, 12*time.Hour, objPath.GetAttr("duration"), &diags)
|
||||
}
|
||||
|
||||
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"),
|
||||
))
|
||||
}
|
||||
validateNonEmptyString(val, objPath.GetAttr("external_id"), &diags)
|
||||
}
|
||||
|
||||
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"),
|
||||
))
|
||||
}
|
||||
validateNonEmptyString(val, objPath.GetAttr("policy"), &diags)
|
||||
}
|
||||
|
||||
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"),
|
||||
))
|
||||
}
|
||||
validateNonEmptyString(val, objPath.GetAttr("session_name"), &diags)
|
||||
}
|
||||
|
||||
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"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
validatePolicyARNSlice(val, objPath.GetAttr("policy_arns"), &diags)
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func validateAssumeRoleWithWebIdentity(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
validateAttributesConflict(
|
||||
cty.GetAttrPath("web_identity_token"),
|
||||
cty.GetAttrPath("web_identity_token_file"),
|
||||
)(obj, objPath, &diags)
|
||||
|
||||
if val, ok := stringAttrOk(obj, "session_name"); ok {
|
||||
validateNonEmptyString(val, objPath.GetAttr("session_name"), &diags)
|
||||
}
|
||||
|
||||
if val, ok := stringAttrOk(obj, "policy"); ok {
|
||||
validateNonEmptyString(val, objPath.GetAttr("policy"), &diags)
|
||||
}
|
||||
|
||||
if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok {
|
||||
validatePolicyARNSlice(val, objPath.GetAttr("policy_arns"), &diags)
|
||||
}
|
||||
|
||||
if val, ok := stringAttrOk(obj, "duration"); ok {
|
||||
validateDuration(val, 15*time.Minute, 12*time.Hour, objPath.GetAttr("duration"), &diags)
|
||||
}
|
||||
|
||||
return diags
|
||||
@ -222,3 +195,54 @@ func attributeErrDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnos
|
||||
func attributeWarningDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnostic {
|
||||
return tfdiags.AttributeValue(tfdiags.Warning, summary, detail, attrPath.Copy())
|
||||
}
|
||||
|
||||
func validateNonEmptyString(val string, path cty.Path, diags *tfdiags.Diagnostics) {
|
||||
if len(strings.TrimSpace(val)) == 0 {
|
||||
*diags = diags.Append(attributeErrDiag(
|
||||
"Invalid Value",
|
||||
"The value cannot be empty or all whitespace",
|
||||
path,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func validatePolicyARNSlice(val []string, path cty.Path, diags *tfdiags.Diagnostics) {
|
||||
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),
|
||||
path,
|
||||
))
|
||||
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),
|
||||
path,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateDuration(val string, min, max time.Duration, path cty.Path, diags *tfdiags.Diagnostics) {
|
||||
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,
|
||||
))
|
||||
return
|
||||
}
|
||||
if (min > 0 && d < min) || (max > 0 && d > max) {
|
||||
*diags = diags.Append(attributeErrDiag(
|
||||
"Invalid Duration",
|
||||
fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, val),
|
||||
path,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -227,6 +227,74 @@ terraform {
|
||||
}
|
||||
```
|
||||
|
||||
#### Assume Role With Web Identity Configuration
|
||||
|
||||
The following `assume_role_with_web_identity` configuration block is optional:
|
||||
|
||||
* `role_arn` - (Required) Amazon Resource Name (ARN) of the IAM Role to assume.
|
||||
Can also be set with the `AWS_ROLE_ARN` environment variable.
|
||||
* `duration` - (Optional) The duration individual credentials will be valid.
|
||||
Credentials are automatically renewed up to the maximum defined by the AWS account.
|
||||
Specified using the format `<hours>h<minutes>m<seconds>s` with any unit being optional.
|
||||
For example, an hour and a half can be specified as `1h30m` or `90m`.
|
||||
Must be between 15 minutes (15m) and 12 hours (12h).
|
||||
* `policy` - (Optional) IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.
|
||||
* `policy_arns` - (Optional) Set of Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.
|
||||
* `session_name` - (Optional) Session name to use when assuming the role.
|
||||
Can also be set with the `AWS_ROLE_SESSION_NAME` environment variable.
|
||||
* `web_identity_token` - (Optional) The value of a web identity token from an OpenID Connect (OIDC) or OAuth provider.
|
||||
One of `web_identity_token` or `web_identity_token_file` is required.
|
||||
* `web_identity_token_file` - (Optional) File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider.
|
||||
One of `web_identity_token_file` or `web_identity_token` is required.
|
||||
Can also be set with the `AWS_WEB_IDENTITY_TOKEN_FILE` environment variable.
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "mybucket"
|
||||
key = "my/key.tfstate"
|
||||
region = "us-east-1"
|
||||
|
||||
assume_role_with_web_identity = {
|
||||
role_arn = "arn:aws:iam::ACCOUNT-ID:role/Opentofu"
|
||||
web_identity_token = "<token value>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It's possible to constrain the assumed role by providing a policy.
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "mybucket"
|
||||
key = "my/key.tfstate"
|
||||
region = "us-east-1"
|
||||
|
||||
assume_role_with_web_identity = {
|
||||
role_arn = "arn:aws:iam::ACCOUNT-ID:role/Opentofu"
|
||||
web_identity_token = "<token value>"
|
||||
policy = <<-JSON
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:*",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::mybucket/*",
|
||||
"arn:aws:s3:::mybucket"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### S3 State Storage
|
||||
|
||||
The following configuration is required:
|
||||
|
Loading…
Reference in New Issue
Block a user