Skip to content

Commit

Permalink
Add IAM tagging support for iam_user roles in AWS secret engine (#10953)
Browse files Browse the repository at this point in the history
* Added support for iam_tags for AWS secret roles

This change allows iam_users generated by the secrets engine
to add custom tags in the form of key-value pairs to users
that are created.
  • Loading branch information
Lauren Voswinkel authored Feb 26, 2021
1 parent 8bb439f commit eece14e
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 16 deletions.
59 changes: 59 additions & 0 deletions builtin/logical/aws/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
41 changes: 29 additions & 12 deletions builtin/logical/aws/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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{} {
Expand All @@ -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,
Expand Down
30 changes: 26 additions & 4 deletions builtin/logical/aws/secret_access_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
49 changes: 49 additions & 0 deletions website/content/api-docs/secret/aws.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -329,6 +335,49 @@ Using groups:
}
```

Using tags:
<Tabs>
<Tab heading="cURL">
```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"
}
}
```
</Tab>
<Tab heading="CLI">
```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"]
```
</Tab>
</Tabs>

## Read Role

This endpoint queries an existing role by the given name. If the role does not
Expand Down

0 comments on commit eece14e

Please sign in to comment.