diff --git a/builtin/logical/aws/backend_test.go b/builtin/logical/aws/backend_test.go index 6ec153cfd..57d85054e 100644 --- a/builtin/logical/aws/backend_test.go +++ b/builtin/logical/aws/backend_test.go @@ -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{ @@ -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) @@ -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) }, @@ -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}), }, }) @@ -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) diff --git a/builtin/logical/aws/path_roles.go b/builtin/logical/aws/path_roles.go index 87f94374e..631f25936 100644 --- a/builtin/logical/aws/path_roles.go +++ b/builtin/logical/aws/path_roles.go @@ -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 } diff --git a/builtin/logical/aws/path_roles_test.go b/builtin/logical/aws/path_roles_test.go index 3ac2473e1..a22c9ae1b 100644 --- a/builtin/logical/aws/path_roles_test.go +++ b/builtin/logical/aws/path_roles_test.go @@ -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)) + } + }) + } +} diff --git a/builtin/logical/aws/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index 81a828a31..1dbce4c43 100644 --- a/builtin/logical/aws/secret_access_keys.go +++ b/builtin/logical/aws/secret_access_keys.go @@ -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 { diff --git a/website/source/api/secret/aws/index.html.md b/website/source/api/secret/aws/index.html.md index f790b6e2b..aa5dafecd 100644 --- a/website/source/api/secret/aws/index.html.md +++ b/website/source/api/secret/aws/index.html.md @@ -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