AWS backend: Add user_path option for role.
This commit is contained in:
parent
0a9b503f67
commit
67f5bbe88f
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue