mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 03:58:01 +00:00
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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user