AWS backend: Add user_path option for role.

This commit is contained in:
Povilas Susinskas 2019-03-31 15:10:17 +02:00 committed by Povilas Susinskas
parent 0a9b503f67
commit 67f5bbe88f
5 changed files with 143 additions and 19 deletions

View File

@ -7,6 +7,7 @@ import (
"net/http"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
@ -556,6 +557,27 @@ func describeAzsTestUnauthorized(accessKey, secretKey, token string) error {
})
}
func assertCreatedIAMUser(accessKey, secretKey, token string) error {
creds := credentials.NewStaticCredentials(accessKey, secretKey, token)
awsConfig := &aws.Config{
Credentials: creds,
Region: aws.String("us-east-1"),
HTTPClient: cleanhttp.DefaultClient(),
}
client := iam.New(session.New(awsConfig))
log.Printf("[WARN] Checking if IAM User is created properly...")
userOutput, err := client.GetUser(&iam.GetUserInput{})
if err != nil {
return err
}
if *userOutput.User.Path != "/path/" {
return fmt.Errorf("bad: got: %#v\nexpected: %#v", userOutput.User.Path, "/path/")
}
return nil
}
func listIamUsersTest(accessKey, secretKey, token string) error {
creds := credentials.NewStaticCredentials(accessKey, secretKey, token)
awsConfig := &aws.Config{
@ -650,9 +672,10 @@ func testAccStepReadPolicy(t *testing.T, name string, value string) logicaltest.
"policy_arns": []string(nil),
"role_arns": []string(nil),
"policy_document": value,
"credential_types": []string{iamUserCred, federationTokenCred},
"credential_type": strings.Join([]string{iamUserCred, federationTokenCred}, ","),
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "",
}
if !reflect.DeepEqual(resp.Data, expected) {
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
@ -743,15 +766,18 @@ func TestBackend_iamUserManagedInlinePolicies(t *testing.T) {
"policy_document": testDynamoPolicy,
"policy_arns": []string{ec2PolicyArn, iamPolicyArn},
"credential_type": iamUserCred,
"user_path": "/path/",
}
expectedRoleData := map[string]interface{}{
"policy_document": compacted,
"policy_arns": []string{ec2PolicyArn, iamPolicyArn},
"credential_types": []string{iamUserCred},
"credential_type": iamUserCred,
"role_arns": []string(nil),
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "/path/",
}
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
PreCheck: func() { testAccPreCheck(t) },
@ -760,7 +786,7 @@ func TestBackend_iamUserManagedInlinePolicies(t *testing.T) {
testAccStepConfig(t),
testAccStepWriteRole(t, "test", roleData),
testAccStepReadRole(t, "test", expectedRoleData),
testAccStepRead(t, "creds", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest}),
testAccStepRead(t, "creds", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest, assertCreatedIAMUser}),
testAccStepRead(t, "sts", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest}),
},
})
@ -884,9 +910,10 @@ func testAccStepReadArnPolicy(t *testing.T, name string, value string) logicalte
"policy_arns": []string{value},
"role_arns": []string(nil),
"policy_document": "",
"credential_types": []string{iamUserCred},
"credential_type": iamUserCred,
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "",
}
if !reflect.DeepEqual(resp.Data, expected) {
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
@ -16,6 +17,10 @@ import (
"github.com/hashicorp/vault/logical/framework"
)
var (
userPathRegex = regexp.MustCompile(`^\/([\x21-\x7F]{0,510}\/)?$`)
)
func pathListRoles(b *backend) *framework.Path {
return &framework.Path{
Pattern: "roles/?$",
@ -89,6 +94,13 @@ or IAM role to assume`,
Description: "Deprecated; use policy_document instead. IAM policy document",
Deprecated: true,
},
"user_path": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Path for IAM User. Only valid when credential_type is " + iamUserCred,
DisplayName: "User Path",
Default: "/",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -245,6 +257,20 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f
roleEntry.MaxSTSTTL = time.Duration(maxSTSTTLRaw.(int)) * time.Second
}
if userPathRaw, ok := d.GetOk("user_path"); ok {
if legacyRole != "" {
return logical.ErrorResponse("cannot supply deprecated role or policy parameters with user_path"), nil
}
if !strutil.StrListContains(roleEntry.CredentialTypes, iamUserCred) {
return logical.ErrorResponse(fmt.Sprintf("user_path parameter only valid for %s credential type", iamUserCred)), nil
}
if !userPathRegex.MatchString(userPathRaw.(string)) {
return logical.ErrorResponse(fmt.Sprintf("The specified value for user_path is invalid. It must match '%s' regexp", userPathRegex.String())), nil
}
roleEntry.UserPath = userPathRaw.(string)
}
if roleEntry.MaxSTSTTL > 0 &&
roleEntry.DefaultSTSTTL > 0 &&
roleEntry.DefaultSTSTTL > roleEntry.MaxSTSTTL {
@ -432,6 +458,7 @@ type awsRoleEntry struct {
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
}
func (r *awsRoleEntry) toResponseData() map[string]interface{} {
@ -442,7 +469,9 @@ func (r *awsRoleEntry) toResponseData() map[string]interface{} {
"policy_document": r.PolicyDocument,
"default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()),
"max_sts_ttl": int64(r.MaxSTSTTL.Seconds()),
"user_path": r.UserPath,
}
if r.InvalidData != "" {
respData["invalid_data"] = r.InvalidData
}

View File

@ -4,6 +4,7 @@ import (
"context"
"reflect"
"strconv"
"strings"
"testing"
"github.com/hashicorp/vault/logical"
@ -154,3 +155,61 @@ func TestUpgradeLegacyPolicyEntry(t *testing.T) {
t.Fatalf("bad: expected %#v; received %#v", expected, *output)
}
}
func TestUserPathValidity(t *testing.T) {
testCases := []struct {
description string
userPath string
isValid bool
}{
{
description: "Default",
userPath: "/",
isValid: true,
},
{
description: "Empty",
userPath: "",
isValid: false,
},
{
description: "Valid",
userPath: "/path/",
isValid: true,
},
{
description: "Missing leading slash",
userPath: "path/",
isValid: false,
},
{
description: "Missing trailing slash",
userPath: "/path",
isValid: false,
},
{
description: "Invalid character",
userPath: "/šiauliai/",
isValid: false,
},
{
description: "Max length",
userPath: "/" + strings.Repeat("a", 510) + "/",
isValid: true,
},
{
description: "Too long",
userPath: "/" + strings.Repeat("a", 511) + "/",
isValid: false,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
if tc.isValid != userPathRegex.MatchString(tc.userPath) {
t.Fatalf("bad: expected %s", strconv.FormatBool(tc.isValid))
}
})
}
}

View File

@ -180,9 +180,15 @@ func (b *backend) secretAccessKeysCreate(
return nil, errwrap.Wrapf("error writing WAL entry: {{err}}", err)
}
userPath := role.UserPath
if userPath == "" {
userPath = "/"
}
// Create the user
_, err = iamClient.CreateUser(&iam.CreateUserInput{
UserName: aws.String(username),
Path: aws.String(userPath),
})
if err != nil {
if walErr := framework.DeleteWAL(ctx, s, walID); walErr != nil {

View File

@ -230,6 +230,9 @@ updated with the new attributes.
TTL are capped to `max_sts_ttl`). Valid only when `credential_type` is one of
`assumed_role` or `federation_token`.
- `user_path` `(string)` - The path for the user name. Valid only when
`credential_type` is `iam_user`. Default is `/`
Legacy parameters:
These parameters are supported for backwards compatibility only. They cannot be