diff --git a/builtin/logical/aws/backend_test.go b/builtin/logical/aws/backend_test.go index 9876de1cc154..274004399393 100644 --- a/builtin/logical/aws/backend_test.go +++ b/builtin/logical/aws/backend_test.go @@ -1437,6 +1437,65 @@ func testAccStepReadIamGroups(t *testing.T, name string, groups []string) logica } } +func TestBackend_iamTagsCrud(t *testing.T) { + logicaltest.Test(t, logicaltest.TestCase{ + AcceptanceTest: true, + LogicalBackend: getBackend(t), + Steps: []logicaltest.TestStep{ + testAccStepConfig(t), + testAccStepWriteIamTags(t, "test", map[string]string{"key1": "value1", "key2": "value2"}), + testAccStepReadIamTags(t, "test", map[string]string{"key1": "value1", "key2": "value2"}), + testAccStepDeletePolicy(t, "test"), + testAccStepReadIamTags(t, "test", map[string]string{}), + }, + }) +} + +func testAccStepWriteIamTags(t *testing.T, name string, tags map[string]string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "roles/" + name, + Data: map[string]interface{}{ + "credential_type": iamUserCred, + "iam_tags": tags, + }, + } +} + +func testAccStepReadIamTags(t *testing.T, name string, tags map[string]string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "roles/" + name, + Check: func(resp *logical.Response) error { + if resp == nil { + if len(tags) == 0 { + return nil + } + + return fmt.Errorf("vault response not received") + } + + expected := map[string]interface{}{ + "policy_arns": []string(nil), + "role_arns": []string(nil), + "policy_document": "", + "credential_type": iamUserCred, + "default_sts_ttl": int64(0), + "max_sts_ttl": int64(0), + "user_path": "", + "permissions_boundary_arn": "", + "iam_groups": []string(nil), + "iam_tags": tags, + } + if !reflect.DeepEqual(resp.Data, expected) { + return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected) + } + + return nil + }, + } +} + func generateUniqueName(prefix string) string { return testhelpers.RandomWithPrefix(prefix) } diff --git a/builtin/logical/aws/path_roles.go b/builtin/logical/aws/path_roles.go index a5f225479a87..e7dc241b69b7 100644 --- a/builtin/logical/aws/path_roles.go +++ b/builtin/logical/aws/path_roles.go @@ -93,6 +93,17 @@ and policy_arns parameters.`, }, }, + "iam_tags": &framework.FieldSchema{ + Type: framework.TypeKVPairs, + Description: `IAM tags to be set for any users created by this role. These must be presented +as Key-Value pairs. This can be represented as a map or a list of equal sign +delimited key pairs.`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "IAM Tags", + Value: "[key1=value1, key2=value2]", + }, + }, + "default_sts_ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Description: fmt.Sprintf("Default TTL for %s and %s credential types when no TTL is explicitly requested with the credentials", assumedRoleCred, federationTokenCred), @@ -301,6 +312,10 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f roleEntry.IAMGroups = iamGroups.([]string) } + if iamTags, ok := d.GetOk("iam_tags"); ok { + roleEntry.IAMTags = iamTags.(map[string]string) + } + if legacyRole != "" { roleEntry = upgradeLegacyPolicyEntry(legacyRole) if roleEntry.InvalidData != "" { @@ -481,18 +496,19 @@ func setAwsRole(ctx context.Context, s logical.Storage, roleName string, roleEnt } type awsRoleEntry struct { - CredentialTypes []string `json:"credential_types"` // Entries must all be in the set of ("iam_user", "assumed_role", "federation_token") - PolicyArns []string `json:"policy_arns"` // ARNs of managed policies to attach to an IAM user - RoleArns []string `json:"role_arns"` // ARNs of roles to assume for AssumedRole credentials - PolicyDocument string `json:"policy_document"` // JSON-serialized inline policy to attach to IAM users and/or to specify as the Policy parameter in AssumeRole calls - IAMGroups []string `json:"iam_groups"` // Names of IAM groups that generated IAM users will be added to - InvalidData string `json:"invalid_data,omitempty"` // Invalid role data. Exists to support converting the legacy role data into the new format - ProhibitFlexibleCredPath bool `json:"prohibit_flexible_cred_path,omitempty"` // Disallow accessing STS credentials via the creds path and vice verse - Version int `json:"version"` // Version number of the role format - DefaultSTSTTL time.Duration `json:"default_sts_ttl"` // Default TTL for STS credentials - MaxSTSTTL time.Duration `json:"max_sts_ttl"` // Max allowed TTL for STS credentials - UserPath string `json:"user_path"` // The path for the IAM user when using "iam_user" credential type - PermissionsBoundaryARN string `json:"permissions_boundary_arn"` // ARN of an IAM policy to attach as a permissions boundary + CredentialTypes []string `json:"credential_types"` // Entries must all be in the set of ("iam_user", "assumed_role", "federation_token") + PolicyArns []string `json:"policy_arns"` // ARNs of managed policies to attach to an IAM user + RoleArns []string `json:"role_arns"` // ARNs of roles to assume for AssumedRole credentials + PolicyDocument string `json:"policy_document"` // JSON-serialized inline policy to attach to IAM users and/or to specify as the Policy parameter in AssumeRole calls + IAMGroups []string `json:"iam_groups"` // Names of IAM groups that generated IAM users will be added to + IAMTags map[string]string `json:"iam_tags"` // IAM tags that will be added to the generated IAM users + InvalidData string `json:"invalid_data,omitempty"` // Invalid role data. Exists to support converting the legacy role data into the new format + ProhibitFlexibleCredPath bool `json:"prohibit_flexible_cred_path,omitempty"` // Disallow accessing STS credentials via the creds path and vice verse + Version int `json:"version"` // Version number of the role format + DefaultSTSTTL time.Duration `json:"default_sts_ttl"` // Default TTL for STS credentials + MaxSTSTTL time.Duration `json:"max_sts_ttl"` // Max allowed TTL for STS credentials + UserPath string `json:"user_path"` // The path for the IAM user when using "iam_user" credential type + PermissionsBoundaryARN string `json:"permissions_boundary_arn"` // ARN of an IAM policy to attach as a permissions boundary } func (r *awsRoleEntry) toResponseData() map[string]interface{} { @@ -502,6 +518,7 @@ func (r *awsRoleEntry) toResponseData() map[string]interface{} { "role_arns": r.RoleArns, "policy_document": r.PolicyDocument, "iam_groups": r.IAMGroups, + "iam_tags": r.IAMTags, "default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()), "max_sts_ttl": int64(r.MaxSTSTTL.Seconds()), "user_path": r.UserPath, diff --git a/builtin/logical/aws/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index bbc4011c43fc..3a4a3f0afa0f 100644 --- a/builtin/logical/aws/secret_access_keys.go +++ b/builtin/logical/aws/secret_access_keys.go @@ -7,13 +7,14 @@ import ( "regexp" "time" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/awsutil" + "github.com/hashicorp/vault/sdk/logical" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/errwrap" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/helper/awsutil" - "github.com/hashicorp/vault/sdk/logical" ) const secretAccessKeyType = "access_keys" @@ -210,7 +211,8 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage, func (b *backend) secretAccessKeysCreate( ctx context.Context, s logical.Storage, - displayName, policyName string, role *awsRoleEntry) (*logical.Response, error) { + displayName, policyName string, + role *awsRoleEntry) (*logical.Response, error) { iamClient, err := b.clientIAM(ctx, s) if err != nil { return logical.ErrorResponse(err.Error()), nil @@ -286,6 +288,26 @@ func (b *backend) secretAccessKeysCreate( } } + var tags []*iam.Tag + for key, value := range role.IAMTags { + // This assignment needs to be done in order to create unique addresses for + // these variables. Without doing so, all the tags will be copies of the last + // tag listed in the role. + k, v := key, value + tags = append(tags, &iam.Tag{Key: &k, Value: &v}) + } + + if len(tags) > 0 { + _, err = iamClient.TagUser(&iam.TagUserInput{ + Tags: tags, + UserName: &username, + }) + + if err != nil { + return logical.ErrorResponse("Error adding tags to user: %s", err), awsutil.CheckAWSError(err) + } + } + // Create the keys keyResp, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{ UserName: aws.String(username), diff --git a/website/content/api-docs/secret/aws.mdx b/website/content/api-docs/secret/aws.mdx index 2c3da63e3661..5950043892ac 100644 --- a/website/content/api-docs/secret/aws.mdx +++ b/website/content/api-docs/secret/aws.mdx @@ -261,6 +261,12 @@ updated with the new attributes. policies from each group in `iam_groups` combined with the `policy_document` and `policy_arns` parameters. +- `iam_tags` `(list: [])` - A list of strings representing a key/value pair to be used as a + tag for any `iam_user` user that is created by this role. Format is a key and value + separated by an `=` (e.g. `test_key=value`). Note: when using the CLI multiple tags + can be specified in the role configuration by adding another `iam_tags` assignment + in the same command. + - `default_sts_ttl` `(string)` - The default TTL for STS credentials. When a TTL is not specified when STS credentials are requested, and a default TTL is specified on the role, then this default TTL will be used. Valid only when @@ -329,6 +335,49 @@ Using groups: } ``` +Using tags: + + + ```json + { + "credential_type": "iam_user", + "iam_tags": [ + "first_key=first_value", + "second_key=second_value" + ] + } + ``` + or + ```json + { + "credential_type": "iam_user", + "iam_tags": { + "first_key": "first_value", + "second_key": "second_value" + } + } + ``` + + + ```bash + vault write aws/roles/example-role \ + credential_type=iam_user \ + iam_tags="first_key=first_value" \ + iam_tags="second_key=second_value" \ + ``` + or + ```bash + vault write aws/roles/example-role \ + credential_type=iam_user \ + iam_tags=@test.json + ``` + where test.json is + ```json + ["tag1=42", "tag2=something"] + ``` + + + ## Read Role This endpoint queries an existing role by the given name. If the role does not