Skip to content

Commit

Permalink
Merge pull request #6380 from povils/aws_user_path
Browse files Browse the repository at this point in the history
AWS add user_path option for role.
  • Loading branch information
tyrannosaurus-becks authored Apr 23, 2019
2 parents bacefe0 + e1007d1 commit decdbeb
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 19 deletions.
65 changes: 46 additions & 19 deletions builtin/logical/aws/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -647,12 +669,13 @@ func testAccStepReadPolicy(t *testing.T, name string, value string) logicaltest.
}

expected := map[string]interface{}{
"policy_arns": []string(nil),
"role_arns": []string(nil),
"policy_document": value,
"credential_types": []string{iamUserCred, federationTokenCred},
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"policy_arns": []string(nil),
"role_arns": []string(nil),
"policy_document": value,
"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)
Expand Down Expand Up @@ -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},
"role_arns": []string(nil),
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"policy_document": compacted,
"policy_arns": []string{ec2PolicyArn, iamPolicyArn},
"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) },
Expand All @@ -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}),
},
})
Expand Down Expand Up @@ -881,12 +907,13 @@ func testAccStepReadArnPolicy(t *testing.T, name string, value string) logicalte
}

expected := map[string]interface{}{
"policy_arns": []string{value},
"role_arns": []string(nil),
"policy_document": "",
"credential_types": []string{iamUserCred},
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"policy_arns": []string{value},
"role_arns": []string(nil),
"policy_document": "",
"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)
Expand Down
29 changes: 29 additions & 0 deletions builtin/logical/aws/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"

Expand All @@ -16,6 +17,10 @@ import (
"github.com/hashicorp/vault/sdk/logical"
)

var (
userPathRegex = regexp.MustCompile(`^\/([\x21-\x7F]{0,510}\/)?$`)
)

func pathListRoles(b *backend) *framework.Path {
return &framework.Path{
Pattern: "roles/?$",
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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{} {
Expand All @@ -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
}
Expand Down
59 changes: 59 additions & 0 deletions builtin/logical/aws/path_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"reflect"
"strconv"
"strings"
"testing"

"github.com/hashicorp/vault/sdk/logical"
Expand Down Expand Up @@ -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))
}
})
}
}
6 changes: 6 additions & 0 deletions builtin/logical/aws/secret_access_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions website/source/api/secret/aws/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit decdbeb

Please sign in to comment.