Add IAM tagging support for iam_user roles in AWS secret engine (#10953)

* 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.
This commit is contained in:
Lauren Voswinkel
2021-02-25 16:03:24 -08:00
committed by GitHub
parent 8bb439fc7e
commit eece14e7c9
4 changed files with 163 additions and 16 deletions

View File

@@ -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 { func generateUniqueName(prefix string) string {
return testhelpers.RandomWithPrefix(prefix) return testhelpers.RandomWithPrefix(prefix)
} }

View File

@@ -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{ "default_sts_ttl": &framework.FieldSchema{
Type: framework.TypeDurationSecond, 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), 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) roleEntry.IAMGroups = iamGroups.([]string)
} }
if iamTags, ok := d.GetOk("iam_tags"); ok {
roleEntry.IAMTags = iamTags.(map[string]string)
}
if legacyRole != "" { if legacyRole != "" {
roleEntry = upgradeLegacyPolicyEntry(legacyRole) roleEntry = upgradeLegacyPolicyEntry(legacyRole)
if roleEntry.InvalidData != "" { if roleEntry.InvalidData != "" {
@@ -481,18 +496,19 @@ func setAwsRole(ctx context.Context, s logical.Storage, roleName string, roleEnt
} }
type awsRoleEntry struct { type awsRoleEntry struct {
CredentialTypes []string `json:"credential_types"` // Entries must all be in the set of ("iam_user", "assumed_role", "federation_token") 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 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 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 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 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 IAMTags map[string]string `json:"iam_tags"` // IAM tags that will be added to the generated IAM users
ProhibitFlexibleCredPath bool `json:"prohibit_flexible_cred_path,omitempty"` // Disallow accessing STS credentials via the creds path and vice verse InvalidData string `json:"invalid_data,omitempty"` // Invalid role data. Exists to support converting the legacy role data into the new format
Version int `json:"version"` // Version number of the role format ProhibitFlexibleCredPath bool `json:"prohibit_flexible_cred_path,omitempty"` // Disallow accessing STS credentials via the creds path and vice verse
DefaultSTSTTL time.Duration `json:"default_sts_ttl"` // Default TTL for STS credentials Version int `json:"version"` // Version number of the role format
MaxSTSTTL time.Duration `json:"max_sts_ttl"` // Max allowed TTL for STS credentials DefaultSTSTTL time.Duration `json:"default_sts_ttl"` // Default TTL for STS credentials
UserPath string `json:"user_path"` // The path for the IAM user when using "iam_user" credential type MaxSTSTTL time.Duration `json:"max_sts_ttl"` // Max allowed TTL for STS credentials
PermissionsBoundaryARN string `json:"permissions_boundary_arn"` // ARN of an IAM policy to attach as a permissions boundary 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{} { func (r *awsRoleEntry) toResponseData() map[string]interface{} {
@@ -502,6 +518,7 @@ func (r *awsRoleEntry) toResponseData() map[string]interface{} {
"role_arns": r.RoleArns, "role_arns": r.RoleArns,
"policy_document": r.PolicyDocument, "policy_document": r.PolicyDocument,
"iam_groups": r.IAMGroups, "iam_groups": r.IAMGroups,
"iam_tags": r.IAMTags,
"default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()), "default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()),
"max_sts_ttl": int64(r.MaxSTSTTL.Seconds()), "max_sts_ttl": int64(r.MaxSTSTTL.Seconds()),
"user_path": r.UserPath, "user_path": r.UserPath,

View File

@@ -7,13 +7,14 @@ import (
"regexp" "regexp"
"time" "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/aws"
"github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts" "github.com/aws/aws-sdk-go/service/sts"
"github.com/hashicorp/errwrap" "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" const secretAccessKeyType = "access_keys"
@@ -210,7 +211,8 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
func (b *backend) secretAccessKeysCreate( func (b *backend) secretAccessKeysCreate(
ctx context.Context, ctx context.Context,
s logical.Storage, 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) iamClient, err := b.clientIAM(ctx, s)
if err != nil { if err != nil {
return logical.ErrorResponse(err.Error()), 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 // Create the keys
keyResp, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{ keyResp, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{
UserName: aws.String(username), UserName: aws.String(username),

View File

@@ -261,6 +261,12 @@ updated with the new attributes.
policies from each group in `iam_groups` combined with the `policy_document` policies from each group in `iam_groups` combined with the `policy_document`
and `policy_arns` parameters. 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 - `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 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 on the role, then this default TTL will be used. Valid only when
@@ -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 ## Read Role
This endpoint queries an existing role by the given name. If the role does not This endpoint queries an existing role by the given name. If the role does not