Adds `singleNestedAttribute` schema attribute and removes custom validation functions for assume role attributes

pull/33872/head
Graham Davison 3 years ago
parent a604129aec
commit 12b8a5c851

@ -304,19 +304,9 @@ func (b *Backend) ConfigSchema() *configschema.Block {
Description: "The maximum number of times an AWS API request is retried on retryable failure.",
},
"assume_role": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: assumeRoleFullSchema().SchemaAttributes(),
},
},
"assume_role": assumeRoleSchema.SchemaAttribute(),
"assume_role_with_web_identity": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: assumeRoleWithWebIdentityFullSchema().SchemaAttributes(),
},
},
"assume_role_with_web_identity": assumeRoleWithWebIdentitySchema.SchemaAttribute(),
"use_legacy_workflow": {
Type: cty.Bool,
@ -354,6 +344,249 @@ func (b *Backend) ConfigSchema() *configschema.Block {
}
}
var assumeRoleSchema = singleNestedAttribute{
Attributes: map[string]schemaAttribute{
"role_arn": stringAttribute{
configschema.Attribute{
Type: cty.String,
Required: true,
Description: "The role to be assumed.",
},
validateString{
Validators: []stringValidator{
validateARN(
validateIAMRoleARN,
),
},
},
},
"duration": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.",
},
validateString{
Validators: []stringValidator{
validateDuration(
validateDurationBetween(15*time.Minute, 12*time.Hour),
),
},
},
},
"external_id": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The external ID to use when assuming the role",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(2, 1224),
validateStringMatches(
regexp.MustCompile(`^[\w+=,.@:\/\-]*$`),
`Value can only contain letters, numbers, or the following characters: =,.@/-`,
),
},
},
},
"policy": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
},
validateString{
Validators: []stringValidator{
validateStringNotEmpty,
validateIAMPolicyDocument,
},
},
},
"policy_arns": setAttribute{
configschema.Attribute{
Type: cty.Set(cty.String),
Optional: true,
Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
},
validateSet{
Validators: []setValidator{
validateSetStringElements(
validateARN(
validateIAMPolicyARN,
),
),
},
},
},
"session_name": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The session name to use when assuming the role.",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(2, 64),
validateStringMatches(
regexp.MustCompile(`^[\w+=,.@\-]*$`),
`Value can only contain letters, numbers, or the following characters: =,.@-`,
),
},
},
},
// NOT SUPPORTED by `aws-sdk-go-base/v1`
// "source_identity": stringAttribute{
// configschema.Attribute{
// Type: cty.String,
// Optional: true,
// Description: "Source identity specified by the principal assuming the role.",
// ValidateFunc: validAssumeRoleSourceIdentity,
// },
// },
"tags": mapAttribute{
configschema.Attribute{
Type: cty.Map(cty.String),
Optional: true,
Description: "Assume role session tags.",
},
validateMap{},
},
"transitive_tag_keys": setAttribute{
configschema.Attribute{
Type: cty.Set(cty.String),
Optional: true,
Description: "Assume role session tag keys to pass to any subsequent sessions.",
},
validateSet{},
},
},
}
var assumeRoleWithWebIdentitySchema = singleNestedAttribute{
Attributes: map[string]schemaAttribute{
"role_arn": stringAttribute{
configschema.Attribute{
Type: cty.String,
Required: true,
Description: "The role to be assumed.",
},
validateString{
Validators: []stringValidator{
validateARN(
validateIAMRoleARN,
),
},
},
},
"duration": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.",
},
validateString{
Validators: []stringValidator{
validateDuration(
validateDurationBetween(15*time.Minute, 12*time.Hour),
),
},
},
},
"policy": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
},
validateString{
Validators: []stringValidator{
validateStringNotEmpty,
validateIAMPolicyDocument,
},
},
},
"policy_arns": setAttribute{
configschema.Attribute{
Type: cty.Set(cty.String),
Optional: true,
Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
},
validateSet{
Validators: []setValidator{
validateSetStringElements(
validateARN(
validateIAMPolicyARN,
),
),
},
},
},
"session_name": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The session name to use when assuming the role.",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(2, 64),
validateStringMatches(
regexp.MustCompile(`^[\w+=,.@\-]*$`),
`Value can only contain letters, numbers, or the following characters: =,.@-`,
),
},
},
},
"web_identity_token": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "Value of a web identity token from an OpenID Connect (OIDC) or OAuth provider.",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(4, 20000),
},
},
},
"web_identity_token_file": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider.",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(4, 20000),
},
},
},
},
validateObject: validateObject{
Validators: []objectValidator{
validateExactlyOneOfAttributes(
cty.GetAttrPath("web_identity_token"),
cty.GetAttrPath("web_identity_token_file"),
),
},
},
}
// 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
@ -440,7 +673,7 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics)
}
if val := obj.GetAttr("assume_role"); !val.IsNull() {
diags = diags.Append(prepareAssumeRoleConfig(val, cty.GetAttrPath("assume_role")))
diags = diags.Append(validateNestedAttribute(assumeRoleSchema, val, cty.GetAttrPath("assume_role")))
if defined := findDeprecatedFields(obj, assumeRoleDeprecatedFields); len(defined) != 0 {
diags = diags.Append(tfdiags.WholeContainingBody(
@ -461,7 +694,7 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics)
}
if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() {
diags = diags.Append(prepareAssumeRoleWithWebIdentityConfig(val, cty.GetAttrPath("assume_role_with_web_identity")))
diags = diags.Append(validateNestedAttribute(assumeRoleWithWebIdentitySchema, val, cty.GetAttrPath("assume_role_with_web_identity")))
}
validateAttributesConflict(
@ -1196,45 +1429,22 @@ The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS)
while "AWS_SSE_CUSTOMER_KEY" is used for encryption with customer-managed keys (SSE-C).
Please choose one or the other.`
func prepareAssumeRoleConfig(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
// TODO: make diags a parameter
func validateNestedAttribute(objSchema schemaAttribute, obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if obj.IsNull() {
return diags
}
for name, attrSchema := range assumeRoleFullSchema() {
attrPath := objPath.GetAttr(name)
attrVal := obj.GetAttr(name)
if a, e := attrVal.Type(), attrSchema.SchemaAttribute().Type; a != e {
diags = diags.Append(attributeErrDiag(
"Internal Error",
fmt.Sprintf(`Expected type to be %s, got: %s`, e.FriendlyName(), a.FriendlyName()),
attrPath,
))
continue
}
if attrVal.IsNull() {
if attrSchema.SchemaAttribute().Required {
diags = diags.Append(requiredAttributeErrDiag(attrPath))
}
continue
}
validator := attrSchema.Validator()
validator.ValidateAttr(attrVal, attrPath, &diags)
}
return diags
}
func prepareAssumeRoleWithWebIdentityConfig(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if obj.IsNull() {
na, ok := objSchema.(singleNestedAttribute)
if !ok {
return diags
}
for name, attrSchema := range assumeRoleWithWebIdentityFullSchema() {
validator := objSchema.Validator()
validator.ValidateAttr(obj, objPath, &diags)
for name, attrSchema := range na.Attributes {
attrPath := objPath.GetAttr(name)
attrVal := obj.GetAttr(name)
@ -1258,11 +1468,6 @@ func prepareAssumeRoleWithWebIdentityConfig(obj cty.Value, objPath cty.Path) tfd
validator.ValidateAttr(attrVal, attrPath, &diags)
}
validateExactlyOneOfAttributes(
cty.GetAttrPath("web_identity_token"),
cty.GetAttrPath("web_identity_token_file"),
)(obj, objPath, &diags)
return diags
}
@ -1343,11 +1548,23 @@ func (v validateSet) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdia
}
}
type validateObject struct {
Validators []objectValidator
}
func (v validateObject) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) {
for _, validator := range v.Validators {
validator(val, attrPath, diags)
}
}
type schemaAttribute interface {
SchemaAttribute() *configschema.Attribute
Validator() validateSchema
}
var _ schemaAttribute = stringAttribute{}
type stringAttribute struct {
configschema.Attribute
validateString
@ -1361,6 +1578,8 @@ func (a stringAttribute) Validator() validateSchema {
return a.validateString
}
var _ schemaAttribute = setAttribute{}
type setAttribute struct {
configschema.Attribute
validateSet
@ -1374,6 +1593,8 @@ func (a setAttribute) Validator() validateSchema {
return a.validateSet
}
var _ schemaAttribute = mapAttribute{}
type mapAttribute struct {
configschema.Attribute
validateMap
@ -1397,239 +1618,24 @@ func (s objectSchema) SchemaAttributes() map[string]*configschema.Attribute {
return m
}
func assumeRoleFullSchema() objectSchema {
return map[string]schemaAttribute{
"role_arn": stringAttribute{
configschema.Attribute{
Type: cty.String,
Required: true,
Description: "The role to be assumed.",
},
validateString{
Validators: []stringValidator{
validateARN(
validateIAMRoleARN,
),
},
},
},
var _ schemaAttribute = singleNestedAttribute{}
"duration": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.",
},
validateString{
Validators: []stringValidator{
validateDuration(
validateDurationBetween(15*time.Minute, 12*time.Hour),
),
},
},
},
"external_id": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The external ID to use when assuming the role",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(2, 1224),
validateStringMatches(
regexp.MustCompile(`^[\w+=,.@:\/\-]*$`),
`Value can only contain letters, numbers, or the following characters: =,.@/-`,
),
},
},
},
"policy": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
},
validateString{
Validators: []stringValidator{
validateStringNotEmpty,
validateIAMPolicyDocument,
},
},
},
"policy_arns": setAttribute{
configschema.Attribute{
Type: cty.Set(cty.String),
Optional: true,
Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
},
validateSet{
Validators: []setValidator{
validateSetStringElements(
validateARN(
validateIAMPolicyARN,
),
),
},
},
},
"session_name": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The session name to use when assuming the role.",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(2, 64),
validateStringMatches(
regexp.MustCompile(`^[\w+=,.@\-]*$`),
`Value can only contain letters, numbers, or the following characters: =,.@-`,
),
},
},
},
// NOT SUPPORTED by `aws-sdk-go-base/v1`
// "source_identity": stringAttribute{
// configschema.Attribute{
// Type: cty.String,
// Optional: true,
// Description: "Source identity specified by the principal assuming the role.",
// ValidateFunc: validAssumeRoleSourceIdentity,
// },
// },
type singleNestedAttribute struct {
Attributes objectSchema
validateObject
}
"tags": mapAttribute{
configschema.Attribute{
Type: cty.Map(cty.String),
Optional: true,
Description: "Assume role session tags.",
},
validateMap{},
},
"transitive_tag_keys": setAttribute{
configschema.Attribute{
Type: cty.Set(cty.String),
Optional: true,
Description: "Assume role session tag keys to pass to any subsequent sessions.",
},
validateSet{},
func (a singleNestedAttribute) SchemaAttribute() *configschema.Attribute {
return &configschema.Attribute{
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: a.Attributes.SchemaAttributes(),
},
}
}
func assumeRoleWithWebIdentityFullSchema() objectSchema {
return map[string]schemaAttribute{
"role_arn": stringAttribute{
configschema.Attribute{
Type: cty.String,
Required: true,
Description: "The role to be assumed.",
},
validateString{
Validators: []stringValidator{
validateARN(
validateIAMRoleARN,
),
},
},
},
"duration": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.",
},
validateString{
Validators: []stringValidator{
validateDuration(
validateDurationBetween(15*time.Minute, 12*time.Hour),
),
},
},
},
"policy": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
},
validateString{
Validators: []stringValidator{
validateStringNotEmpty,
validateIAMPolicyDocument,
},
},
},
"policy_arns": setAttribute{
configschema.Attribute{
Type: cty.Set(cty.String),
Optional: true,
Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
},
validateSet{
Validators: []setValidator{
validateSetStringElements(
validateARN(
validateIAMPolicyARN,
),
),
},
},
},
"session_name": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "The session name to use when assuming the role.",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(2, 64),
validateStringMatches(
regexp.MustCompile(`^[\w+=,.@\-]*$`),
`Value can only contain letters, numbers, or the following characters: =,.@-`,
),
},
},
},
"web_identity_token": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "Value of a web identity token from an OpenID Connect (OIDC) or OAuth provider.",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(4, 20000),
},
},
},
"web_identity_token_file": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider.",
},
validateString{
Validators: []stringValidator{
validateStringLenBetween(4, 20000),
},
},
},
}
func (a singleNestedAttribute) Validator() validateSchema {
return a.validateObject
}
func deprecatedAttrDiag(attr, replacement cty.Path) tfdiags.Diagnostic {

@ -1872,14 +1872,14 @@ web_identity_token_file = no-such-file
ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{
attributeErrDiag(
"Missing Required Value",
`The attribute "assume_role_with_web_identity.role_arn" is required by the backend.`+"\n\n"+
"Refer to the backend documentation for additional information which attributes are required.",
cty.GetAttrPath("assume_role_with_web_identity").GetAttr("role_arn"),
`Exactly one of web_identity_token, web_identity_token_file must be set.`,
cty.GetAttrPath("assume_role_with_web_identity"),
),
attributeErrDiag(
"Missing Required Value",
`Exactly one of web_identity_token, web_identity_token_file must be set.`,
cty.GetAttrPath("assume_role_with_web_identity"),
`The attribute "assume_role_with_web_identity.role_arn" is required by the backend.`+"\n\n"+
"Refer to the backend documentation for additional information which attributes are required.",
cty.GetAttrPath("assume_role_with_web_identity").GetAttr("role_arn"),
),
}),
},

@ -1971,7 +1971,7 @@ func TestAssumeRole_PrepareConfigValidation(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
schema := assumeRoleFullSchema()
schema := assumeRoleSchema.Attributes
vals := make(map[string]cty.Value, len(schema))
for name, attrSchema := range schema {
if val, ok := tc.config[name]; ok {
@ -1982,7 +1982,7 @@ func TestAssumeRole_PrepareConfigValidation(t *testing.T) {
}
config := cty.ObjectVal(vals)
diags := prepareAssumeRoleConfig(config, path)
diags := validateNestedAttribute(assumeRoleSchema, config, path)
if diff := cmp.Diff(diags, tc.expectedDiags, cmp.Comparer(diagnosticComparer)); diff != "" {
t.Errorf("unexpected diagnostics difference: %s", diff)

Loading…
Cancel
Save