Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS add user_path option for role. #6380

Merged
merged 1 commit into from
Apr 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/logical/framework"
)

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/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