diff --git a/builtin/logical/aws/path_roles.go b/builtin/logical/aws/path_roles.go index 67545e641434..b8902e8fffd5 100644 --- a/builtin/logical/aws/path_roles.go +++ b/builtin/logical/aws/path_roles.go @@ -91,7 +91,6 @@ user generated. When credential_type is assumed_role or federation_token, this will be passed in as the Policy parameter to the AssumeRole or GetFederationToken API call, acting as a filter on permissions available.`, }, - "iam_groups": { Type: framework.TypeCommaStringSlice, Description: `Names of IAM groups that generated IAM users will be added to. For a credential @@ -115,7 +114,23 @@ delimited key pairs.`, Value: "[key1=value1, key2=value2]", }, }, - + "session_tags": { + Type: framework.TypeKVPairs, + Description: fmt.Sprintf(`Session tags to be set for %q creds 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.`, assumedRoleCred), + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Session Tags", + Value: "[key1=value1, key2=value2]", + }, + }, + "external_id": { + Type: framework.TypeString, + Description: "External ID to set when assuming the role; only valid when credential_type is" + assumedRoleCred, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "External ID", + }, + }, "default_sts_ttl": { 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), @@ -328,6 +343,14 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f roleEntry.IAMTags = iamTags.(map[string]string) } + if sessionTags, ok := d.GetOk("session_tags"); ok { + roleEntry.SessionTags = sessionTags.(map[string]string) + } + + if externalID, ok := d.GetOk("external_id"); ok { + roleEntry.ExternalID = externalID.(string) + } + if legacyRole != "" { roleEntry = upgradeLegacyPolicyEntry(legacyRole) if roleEntry.InvalidData != "" { @@ -514,6 +537,8 @@ type awsRoleEntry struct { 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 + SessionTags map[string]string `json:"session_tags"` // Session tags that will be added as Tags parameter in AssumedRole calls + ExternalID string `json:"external_id"` // External ID to added as ExternalID in AssumeRole calls 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 @@ -531,6 +556,8 @@ func (r *awsRoleEntry) toResponseData() map[string]interface{} { "policy_document": r.PolicyDocument, "iam_groups": r.IAMGroups, "iam_tags": r.IAMTags, + "session_tags": r.SessionTags, + "external_id": r.ExternalID, "default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()), "max_sts_ttl": int64(r.MaxSTSTTL.Seconds()), "user_path": r.UserPath, @@ -576,7 +603,7 @@ func (r *awsRoleEntry) validate() error { errors = multierror.Append(errors, fmt.Errorf("user_path parameter only valid for %s credential type", iamUserCred)) } if !userPathRegex.MatchString(r.UserPath) { - errors = multierror.Append(errors, fmt.Errorf("The specified value for user_path is invalid. It must match %q regexp", userPathRegex.String())) + errors = multierror.Append(errors, fmt.Errorf("invalid user_path value. It must match %q regexp", userPathRegex.String())) } } @@ -592,6 +619,17 @@ func (r *awsRoleEntry) validate() error { if len(r.RoleArns) > 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) { errors = multierror.Append(errors, fmt.Errorf("cannot supply role_arns when credential_type isn't %s", assumedRoleCred)) } + if len(r.SessionTags) > 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) { + errors = multierror.Append(errors, fmt.Errorf("cannot supply session_tags when credential_type isn't %s", assumedRoleCred)) + // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_know + if len(r.SessionTags) > 50 { + errors = multierror.Append(errors, fmt.Errorf("cannot supply more than %d session_tags", 50)) + } + } + + if r.ExternalID != "" && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) { + errors = multierror.Append(errors, fmt.Errorf("cannot supply external_id when credential_type isn't %s", assumedRoleCred)) + } return errors.ErrorOrNil() } diff --git a/builtin/logical/aws/path_roles_test.go b/builtin/logical/aws/path_roles_test.go index c5bf167866cc..e1da0c79b210 100644 --- a/builtin/logical/aws/path_roles_test.go +++ b/builtin/logical/aws/path_roles_test.go @@ -392,8 +392,13 @@ func TestRoleEntryValidationAssumedRoleCred(t *testing.T) { RoleArns: []string{"arn:aws:iam::123456789012:role/SomeRole"}, PolicyArns: []string{adminAccessPolicyARN}, PolicyDocument: allowAllPolicyDocument, - DefaultSTSTTL: 2, - MaxSTSTTL: 3, + ExternalID: "my-ext-id", + SessionTags: map[string]string{ + "Key1": "Value1", + "Key2": "Value2", + }, + DefaultSTSTTL: 2, + MaxSTSTTL: 3, } if err := roleEntry.validate(); err != nil { t.Errorf("bad: valid roleEntry %#v failed validation: %v", roleEntry, err) diff --git a/builtin/logical/aws/path_user.go b/builtin/logical/aws/path_user.go index 1b6a5cd3b0e3..3dbefe1cd7db 100644 --- a/builtin/logical/aws/path_user.go +++ b/builtin/logical/aws/path_user.go @@ -152,7 +152,7 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr case !strutil.StrListContains(role.RoleArns, roleArn): return logical.ErrorResponse(fmt.Sprintf("role_arn %q not in allowed role arns for Vault role %q", roleArn, roleName)), nil } - return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl, roleSessionName) + return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl, roleSessionName, role.SessionTags, role.ExternalID) case federationTokenCred: return b.getFederationToken(ctx, req.Storage, req.DisplayName, roleName, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl) default: diff --git a/builtin/logical/aws/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index 2f1ac442bcbf..866d82b4e6c3 100644 --- a/builtin/logical/aws/secret_access_keys.go +++ b/builtin/logical/aws/secret_access_keys.go @@ -184,7 +184,7 @@ func (b *backend) getFederationToken(ctx context.Context, s logical.Storage, func (b *backend) assumeRole(ctx context.Context, s logical.Storage, displayName, roleName, roleArn, policy string, policyARNs []string, - iamGroups []string, lifeTimeInSeconds int64, roleSessionName string) (*logical.Response, error, + iamGroups []string, lifeTimeInSeconds int64, roleSessionName string, sessionTags map[string]string, externalID string) (*logical.Response, error, ) { // grab any IAM group policies associated with the vault role, both inline // and managed @@ -241,6 +241,16 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage, if len(policyARNs) > 0 { assumeRoleInput.SetPolicyArns(convertPolicyARNs(policyARNs)) } + if externalID != "" { + assumeRoleInput.SetExternalId(externalID) + } + if len(sessionTags) > 0 { + var tags []*sts.Tag + for k, v := range sessionTags { + tags = append(tags, &sts.Tag{Key: aws.String(k), Value: aws.String(v)}) + } + assumeRoleInput.SetTags(tags) + } tokenResp, err := stsClient.AssumeRoleWithContext(ctx, assumeRoleInput) if err != nil { return logical.ErrorResponse("Error assuming role: %s", err), awsutil.CheckAWSError(err)