mirror of
https://github.com/hashicorp/vault.git
synced 2025-09-10 16:31:07 +02:00
* Make AWS credential types more explicit The AWS secret engine had a lot of confusing overloading with role paramemters and how they mapped to each of the three credential types supported. This now adds parameters to remove the overloading while maintaining backwards compatibility. With the change, it also becomes easier to add other feature requests. Attaching multiple managed policies to IAM users and adding a policy document to STS AssumedRole credentials is now also supported. Fixes #4229 Fixes #3751 Fixes #2817 * Add missing write action to STS endpoint * Allow unsetting policy_document with empty string This allows unsetting the policy_document by passing in an empty string. Previously, it would fail because the empty string isn't a valid JSON document. * Respond to some PR feedback * Refactor and simplify role reading/upgrading This gets rid of the duplicated role upgrade code between both role reading and role writing by handling the upgrade all in the role reading. * Eliminate duplicated AWS secret test code The testAccStepReadUser and testAccStepReadSTS were virtually identical, so they are consolidated into a single method with the path passed in. * Switch to use AWS ARN parser
434 lines
14 KiB
Go
434 lines
14 KiB
Go
package aws
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/arn"
|
|
"github.com/hashicorp/vault/helper/consts"
|
|
"github.com/hashicorp/vault/helper/strutil"
|
|
"github.com/hashicorp/vault/logical"
|
|
"github.com/hashicorp/vault/logical/framework"
|
|
)
|
|
|
|
func pathListRoles(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "roles/?$",
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
logical.ListOperation: b.pathRoleList,
|
|
},
|
|
|
|
HelpSynopsis: pathListRolesHelpSyn,
|
|
HelpDescription: pathListRolesHelpDesc,
|
|
}
|
|
}
|
|
|
|
func pathRoles(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "roles/" + framework.GenericNameRegex("name"),
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"name": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
Description: "Name of the policy",
|
|
},
|
|
|
|
"credential_type": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
Description: fmt.Sprintf("Type of credential to retrieve. Must be one of %s, %s, or %s", assumedRoleCred, iamUserCred, federationTokenCred),
|
|
},
|
|
|
|
"role_arns": &framework.FieldSchema{
|
|
Type: framework.TypeCommaStringSlice,
|
|
Description: "ARNs of AWS roles allowed to be assumed. Only valid when credential_type is " + assumedRoleCred,
|
|
},
|
|
|
|
"policy_arns": &framework.FieldSchema{
|
|
Type: framework.TypeCommaStringSlice,
|
|
Description: "ARNs of AWS policies to attach to IAM users. Only valid when credential_type is " + iamUserCred,
|
|
},
|
|
|
|
"policy_document": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
Description: `JSON-encoded IAM policy document. Behavior varies by credential_type. When credential_type is
|
|
iam_user, then it will attach the contents of the policy_document to the IAM
|
|
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.`,
|
|
},
|
|
|
|
"arn": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
Description: `Deprecated; use role_arns or policy_arns instead. ARN Reference to a managed policy
|
|
or IAM role to assume`,
|
|
},
|
|
|
|
"policy": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
Description: "Deprecated; use policy_document instead. IAM policy document",
|
|
},
|
|
},
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
logical.DeleteOperation: b.pathRolesDelete,
|
|
logical.ReadOperation: b.pathRolesRead,
|
|
logical.UpdateOperation: b.pathRolesWrite,
|
|
},
|
|
|
|
HelpSynopsis: pathRolesHelpSyn,
|
|
HelpDescription: pathRolesHelpDesc,
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
b.roleMutex.RLock()
|
|
defer b.roleMutex.RUnlock()
|
|
entries, err := req.Storage.List(ctx, "role/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
legacyEntries, err := req.Storage.List(ctx, "policy/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return logical.ListResponse(append(entries, legacyEntries...)), nil
|
|
}
|
|
|
|
func (b *backend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
for _, prefix := range []string{"policy/", "role/"} {
|
|
err := req.Storage.Delete(ctx, prefix+d.Get("name").(string))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (b *backend) pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
entry, err := b.roleRead(ctx, req.Storage, d.Get("name").(string), true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entry == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return &logical.Response{
|
|
Data: entry.toResponseData(),
|
|
}, nil
|
|
}
|
|
|
|
func legacyRoleData(d *framework.FieldData) (string, error) {
|
|
policy := d.Get("policy").(string)
|
|
arn := d.Get("arn").(string)
|
|
|
|
switch {
|
|
case policy == "" && arn == "":
|
|
return "", nil
|
|
case policy != "" && arn != "":
|
|
return "", errors.New("only one of policy or arn should be provided")
|
|
case policy != "":
|
|
return policy, nil
|
|
default:
|
|
return arn, nil
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
var resp logical.Response
|
|
|
|
roleName := d.Get("name").(string)
|
|
if roleName == "" {
|
|
return logical.ErrorResponse("missing role name"), nil
|
|
}
|
|
|
|
b.roleMutex.Lock()
|
|
defer b.roleMutex.Unlock()
|
|
roleEntry, err := b.roleRead(ctx, req.Storage, roleName, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if roleEntry == nil {
|
|
roleEntry = &awsRoleEntry{}
|
|
} else if roleEntry.InvalidData != "" {
|
|
resp.AddWarning(fmt.Sprintf("Invalid data of %q cleared out of role", roleEntry.InvalidData))
|
|
roleEntry.InvalidData = ""
|
|
}
|
|
|
|
legacyRole, err := legacyRoleData(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if credentialTypeRaw, ok := d.GetOk("credential_type"); ok {
|
|
if legacyRole != "" {
|
|
return logical.ErrorResponse("cannot supply deprecated role or policy parameters with an explicit credential_type"), nil
|
|
}
|
|
credentialType := credentialTypeRaw.(string)
|
|
allowedCredentialTypes := []string{iamUserCred, assumedRoleCred, federationTokenCred}
|
|
if !strutil.StrListContains(allowedCredentialTypes, credentialType) {
|
|
return logical.ErrorResponse(fmt.Sprintf("unrecognized credential_type: %q, not one of %#v", credentialType, allowedCredentialTypes)), nil
|
|
}
|
|
roleEntry.CredentialTypes = []string{credentialType}
|
|
}
|
|
|
|
if roleArnsRaw, ok := d.GetOk("role_arns"); ok {
|
|
if legacyRole != "" {
|
|
return logical.ErrorResponse("cannot supply deprecated role or policy parameters with role_arns"), nil
|
|
}
|
|
roleEntry.RoleArns = roleArnsRaw.([]string)
|
|
}
|
|
|
|
if policyArnsRaw, ok := d.GetOk("policy_arns"); ok {
|
|
if legacyRole != "" {
|
|
return logical.ErrorResponse("cannot supply deprecated role or policy parameters with policy_arns"), nil
|
|
}
|
|
roleEntry.PolicyArns = policyArnsRaw.([]string)
|
|
}
|
|
|
|
if policyDocumentRaw, ok := d.GetOk("policy_document"); ok {
|
|
if legacyRole != "" {
|
|
return logical.ErrorResponse("cannot supply deprecated role or policy parameters with policy_document"), nil
|
|
}
|
|
compacted := policyDocumentRaw.(string)
|
|
if len(compacted) > 0 {
|
|
compacted, err = compactJSON(policyDocumentRaw.(string))
|
|
if err != nil {
|
|
return logical.ErrorResponse(fmt.Sprintf("cannot parse policy document: %q", policyDocumentRaw.(string))), nil
|
|
}
|
|
}
|
|
roleEntry.PolicyDocument = compacted
|
|
}
|
|
|
|
if legacyRole != "" {
|
|
roleEntry = upgradeLegacyPolicyEntry(legacyRole)
|
|
if roleEntry.InvalidData != "" {
|
|
return logical.ErrorResponse(fmt.Sprintf("unable to parse supplied data: %q", roleEntry.InvalidData)), nil
|
|
}
|
|
resp.AddWarning("Detected use of legacy role or policy paramemter. Please upgrade to use the new parameters.")
|
|
} else {
|
|
roleEntry.ProhibitFlexibleCredPath = false
|
|
}
|
|
|
|
if len(roleEntry.CredentialTypes) == 0 {
|
|
return logical.ErrorResponse("did not supply credential_type"), nil
|
|
}
|
|
|
|
if len(roleEntry.RoleArns) > 0 && !strutil.StrListContains(roleEntry.CredentialTypes, assumedRoleCred) {
|
|
return logical.ErrorResponse(fmt.Sprintf("cannot supply role_arns when credential_type isn't %s", assumedRoleCred)), nil
|
|
}
|
|
if len(roleEntry.PolicyArns) > 0 && !strutil.StrListContains(roleEntry.CredentialTypes, iamUserCred) {
|
|
return logical.ErrorResponse(fmt.Sprintf("cannot supply policy_arns when credential_type isn't %s", iamUserCred)), nil
|
|
}
|
|
|
|
err = setAwsRole(ctx, req.Storage, roleName, roleEntry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
func (b *backend) roleRead(ctx context.Context, s logical.Storage, roleName string, shouldLock bool) (*awsRoleEntry, error) {
|
|
if roleName == "" {
|
|
return nil, fmt.Errorf("missing role name")
|
|
}
|
|
if shouldLock {
|
|
b.roleMutex.RLock()
|
|
}
|
|
entry, err := s.Get(ctx, "role/"+roleName)
|
|
if shouldLock {
|
|
b.roleMutex.RUnlock()
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var roleEntry awsRoleEntry
|
|
if entry != nil {
|
|
if err := entry.DecodeJSON(&roleEntry); err != nil {
|
|
return nil, err
|
|
}
|
|
return &roleEntry, nil
|
|
}
|
|
|
|
if shouldLock {
|
|
b.roleMutex.Lock()
|
|
defer b.roleMutex.Unlock()
|
|
}
|
|
entry, err = s.Get(ctx, "role/"+roleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if entry != nil {
|
|
if err := entry.DecodeJSON(&roleEntry); err != nil {
|
|
return nil, err
|
|
}
|
|
return &roleEntry, nil
|
|
}
|
|
|
|
legacyEntry, err := s.Get(ctx, "policy/"+roleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if legacyEntry == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
newRoleEntry := upgradeLegacyPolicyEntry(string(legacyEntry.Value))
|
|
if b.System().LocalMount() || !b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary) {
|
|
err = setAwsRole(ctx, s, roleName, newRoleEntry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// This can leave legacy data around in the policy/ path if it fails for some reason,
|
|
// but should be pretty rare for this to fail but prior writes to succeed, so not worrying
|
|
// about cleaning it up in case of error
|
|
err = s.Delete(ctx, "policy/"+roleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return newRoleEntry, nil
|
|
}
|
|
|
|
func upgradeLegacyPolicyEntry(entry string) *awsRoleEntry {
|
|
var newRoleEntry *awsRoleEntry
|
|
if strings.HasPrefix(entry, "arn:") {
|
|
parsedArn, err := arn.Parse(entry)
|
|
if err != nil {
|
|
newRoleEntry = &awsRoleEntry{
|
|
InvalidData: entry,
|
|
Version: 1,
|
|
}
|
|
return newRoleEntry
|
|
}
|
|
resourceParts := strings.Split(parsedArn.Resource, "/")
|
|
resourceType := resourceParts[0]
|
|
switch resourceType {
|
|
case "role":
|
|
newRoleEntry = &awsRoleEntry{
|
|
CredentialTypes: []string{assumedRoleCred},
|
|
RoleArns: []string{entry},
|
|
ProhibitFlexibleCredPath: true,
|
|
Version: 1,
|
|
}
|
|
case "policy":
|
|
newRoleEntry = &awsRoleEntry{
|
|
CredentialTypes: []string{iamUserCred},
|
|
PolicyArns: []string{entry},
|
|
ProhibitFlexibleCredPath: true,
|
|
Version: 1,
|
|
}
|
|
default:
|
|
newRoleEntry = &awsRoleEntry{
|
|
InvalidData: entry,
|
|
Version: 1,
|
|
}
|
|
}
|
|
} else {
|
|
compacted, err := compactJSON(entry)
|
|
if err != nil {
|
|
newRoleEntry = &awsRoleEntry{
|
|
InvalidData: entry,
|
|
Version: 1,
|
|
}
|
|
} else {
|
|
// unfortunately, this is ambiguous between the cred types, so allow both
|
|
newRoleEntry = &awsRoleEntry{
|
|
CredentialTypes: []string{iamUserCred, federationTokenCred},
|
|
PolicyDocument: compacted,
|
|
ProhibitFlexibleCredPath: true,
|
|
Version: 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
return newRoleEntry
|
|
}
|
|
|
|
func setAwsRole(ctx context.Context, s logical.Storage, roleName string, roleEntry *awsRoleEntry) error {
|
|
if roleName == "" {
|
|
return fmt.Errorf("empty role name")
|
|
}
|
|
if roleEntry == nil {
|
|
return fmt.Errorf("nil roleEntry")
|
|
}
|
|
entry, err := logical.StorageEntryJSON("role/"+roleName, roleEntry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if entry == nil {
|
|
return fmt.Errorf("nil result when writing to storage")
|
|
}
|
|
if err := s.Put(ctx, entry); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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
|
|
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
|
|
}
|
|
|
|
func (r *awsRoleEntry) toResponseData() map[string]interface{} {
|
|
respData := map[string]interface{}{
|
|
"credential_types": r.CredentialTypes,
|
|
"policy_arns": r.PolicyArns,
|
|
"role_arns": r.RoleArns,
|
|
"policy_document": r.PolicyDocument,
|
|
}
|
|
if r.InvalidData != "" {
|
|
respData["invalid_data"] = r.InvalidData
|
|
}
|
|
return respData
|
|
}
|
|
|
|
func compactJSON(input string) (string, error) {
|
|
var compacted bytes.Buffer
|
|
err := json.Compact(&compacted, []byte(input))
|
|
return compacted.String(), err
|
|
}
|
|
|
|
const (
|
|
assumedRoleCred = "assumed_role"
|
|
iamUserCred = "iam_user"
|
|
federationTokenCred = "federation_token"
|
|
)
|
|
|
|
const pathListRolesHelpSyn = `List the existing roles in this backend`
|
|
|
|
const pathListRolesHelpDesc = `Roles will be listed by the role name.`
|
|
|
|
const pathRolesHelpSyn = `
|
|
Read, write and reference IAM policies that access keys can be made for.
|
|
`
|
|
|
|
const pathRolesHelpDesc = `
|
|
This path allows you to read and write roles that are used to
|
|
create access keys. These roles are associated with IAM policies that
|
|
map directly to the route to read the access keys. For example, if the
|
|
backend is mounted at "aws" and you create a role at "aws/roles/deploy"
|
|
then a user could request access credentials at "aws/creds/deploy".
|
|
|
|
You can either supply a user inline policy (via the policy argument), or
|
|
provide a reference to an existing AWS policy by supplying the full arn
|
|
reference (via the arn argument). Inline user policies written are normal
|
|
IAM policies. Vault will not attempt to parse these except to validate
|
|
that they're basic JSON. No validation is performed on arn references.
|
|
|
|
To validate the keys, attempt to read an access key after writing the policy.
|
|
`
|