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

Update genUsername to cap STS usernames at 32 chars #12185

Merged
merged 12 commits into from Aug 9, 2021
3 changes: 2 additions & 1 deletion builtin/logical/aws/path_config_root.go
Expand Up @@ -8,7 +8,8 @@ import (
"github.com/hashicorp/vault/sdk/logical"
)

const defaultUserNameTemplate = `{{ printf "vault-%s-%s-%s" (printf "%s-%s" (.DisplayName) (.PolicyName) | truncate 42) (unix_time) (random 20) | truncate 64 }}`
// A single default template that supports both the different credential types (IAM/STS) that are capped at differing length limits (64 chars/32 chars respectively)
const defaultUserNameTemplate = `{{ if (eq .Type "STS") }}{{ printf "vault-%s-%s" (unix_time) (random 20) | truncate 32 }}{{ else }}{{ printf "vault-%s-%s-%s" (printf "%s-%s" (.DisplayName) (.PolicyName) | truncate 42) (unix_time) (random 20) | truncate 64 }}{{ end }}`
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved

func pathConfigRoot(b *backend) *framework.Path {
return &framework.Path{
Expand Down
61 changes: 31 additions & 30 deletions builtin/logical/aws/secret_access_keys.go
Expand Up @@ -20,7 +20,6 @@ import (
const (
secretAccessKeyType = "access_keys"
storageKey = "config/root"
defaultSTSTemplate = `{{ printf "vault-%d-%d" (unix_time) (random 20) | truncate 32 }}`
)

func secretAccessKeys(b *backend) *framework.Secret {
Expand All @@ -47,42 +46,45 @@ func secretAccessKeys(b *backend) *framework.Secret {
}
}

func genUsername(displayName, policyName, userType, usernameTemplate string) (ret string, warning string, err error) {
func genUsername(displayName, policyName, userType, usernameTemplate string) (ret string, err error) {
switch userType {
case "iam_user":
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
// IAM users are capped at 64 chars; this leaves, after the beginning and
// end added below, 42 chars to play with.
case "iam_user", "assume_role":
// IAM users are capped at 64 chars
up, err := template.NewTemplate(template.Template(usernameTemplate))
if err != nil {
return "", "", fmt.Errorf("unable to initialize username template: %w", err)
return "", fmt.Errorf("unable to initialize username template: %w", err)
}

um := UsernameMetadata{
Type: "IAM",
DisplayName: normalizeDisplayName(displayName),
PolicyName: normalizeDisplayName(policyName),
}

ret, err = up.Generate(um)
if err != nil {
return "", "", fmt.Errorf("failed to generate username: %w", err)
return "", fmt.Errorf("failed to generate username: %w", err)
}
// To prevent template from exceeding IAM length limits
if len(ret) > 64 {
ret = ret[0:64]
warning = "the calling token display name/IAM policy name were truncated to 64 characters to fit within IAM username length limits"
return "", fmt.Errorf("the username generated by the template exceeds the IAM username length limits of 64 chars")
}
case "sts":
// Capped at 32 chars, which leaves only a couple of characters to play
// with, so don't insert display name or policy name at all
up, err := template.NewTemplate(template.Template(usernameTemplate))
if err != nil {
return "", "", fmt.Errorf("unable to initialize username template: %w", err)
return "", fmt.Errorf("unable to initialize username template: %w", err)
}

um := UsernameMetadata{}
um := UsernameMetadata{
Type: "STS",
}
ret, err = up.Generate(um)
if err != nil {
return "", "", fmt.Errorf("failed to generate username: %w", err)
return "", fmt.Errorf("failed to generate username: %w", err)
}
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
// To prevent template from exceeding STS length limits
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (same above):

Suggested change
// To prevent template from exceeding STS length limits
// To prevent a custom template from exceeding STS length limits

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good call! Updated

if len(ret) > 32 {
return "", fmt.Errorf("the username generated by the template exceeds the STS username length limits of 32 chars")
}
}
return
Expand Down Expand Up @@ -112,7 +114,18 @@ func (b *backend) getFederationToken(ctx context.Context, s logical.Storage,
return logical.ErrorResponse(err.Error()), nil
}

username, usernameWarning, usernameError := genUsername(displayName, policyName, "sts", defaultSTSTemplate)
config, err := readConfig(ctx, s)
if err != nil {
return nil, fmt.Errorf("unable to read configuration: %w", err)
}

// Set as defaultUsernameTemplate if not provided
usernameTemplate := config.UsernameTemplate
if usernameTemplate == "" {
usernameTemplate = defaultUserNameTemplate
}

username, usernameError := genUsername(displayName, policyName, "sts", usernameTemplate)
// Send a 400 to Framework.OperationFunc Handler
if usernameError != nil {
return nil, usernameError
Expand Down Expand Up @@ -158,10 +171,6 @@ func (b *backend) getFederationToken(ctx context.Context, s logical.Storage,
// STS are purposefully short-lived and aren't renewable
resp.Secret.Renewable = false

if usernameWarning != "" {
resp.AddWarning(usernameWarning)
}

return resp, nil
}

Expand Down Expand Up @@ -202,10 +211,9 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
usernameTemplate = defaultUserNameTemplate
}

roleSessionNameWarning := ""
var roleSessionNameError error
if roleSessionName == "" {
roleSessionName, roleSessionNameWarning, roleSessionNameError = genUsername(displayName, roleName, "iam_user", usernameTemplate)
roleSessionName, roleSessionNameError = genUsername(displayName, roleName, "assume_role", usernameTemplate)
// Send a 400 to Framework.OperationFunc Handler
if roleSessionNameError != nil {
return nil, roleSessionNameError
Expand Down Expand Up @@ -247,10 +255,6 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
// STS are purposefully short-lived and aren't renewable
resp.Secret.Renewable = false

if roleSessionNameWarning != "" {
resp.AddWarning(roleSessionNameWarning)
}

return resp, nil
}

Expand Down Expand Up @@ -291,7 +295,7 @@ func (b *backend) secretAccessKeysCreate(
usernameTemplate = defaultUserNameTemplate
}

username, usernameWarning, usernameError := genUsername(displayName, policyName, "iam_user", usernameTemplate)
username, usernameError := genUsername(displayName, policyName, "iam_user", usernameTemplate)
// Send a 400 to Framework.OperationFunc Handler
if usernameError != nil {
return nil, usernameError
Expand Down Expand Up @@ -419,10 +423,6 @@ func (b *backend) secretAccessKeysCreate(
resp.Secret.TTL = lease.Lease
resp.Secret.MaxTTL = lease.LeaseMax

if usernameWarning != "" {
resp.AddWarning(usernameWarning)
}

return resp, nil
}

Expand Down Expand Up @@ -506,6 +506,7 @@ func convertPolicyARNs(policyARNs []string) []*sts.PolicyDescriptorType {
}

type UsernameMetadata struct {
Type string
DisplayName string
PolicyName string
}
111 changes: 64 additions & 47 deletions builtin/logical/aws/secret_access_keys_test.go
Expand Up @@ -47,53 +47,70 @@ func TestNormalizeDisplayName_NormNotRequired(t *testing.T) {
}

func TestGenUsername(t *testing.T) {

testUsername, warning, err := genUsername("name1", "policy1", "iam_user", `{{ printf "vault-%s-%s-%s-%s" (.DisplayName) (.PolicyName) (unix_time) (random 20) | truncate 64 }}`)
if err != nil {
t.Fatalf(
"expected no err; got %s",
err,
)
}

expectedUsernameRegex := `^vault-name1-policy1-[0-9]+-[a-zA-Z0-9]+`
require.Regexp(t, expectedUsernameRegex, testUsername)
// IAM usernames are capped at 64 characters
if len(testUsername) > 64 {
t.Fatalf(
"expected IAM username to be of length 64, got %d",
len(testUsername),
)
}

testUsername, warning, err = genUsername(
"this---is---a---very---long---name",
"long------policy------name",
"iam_user",
`{{ printf "%s-%s-%s-%s" (.DisplayName) (.PolicyName) (unix_time) (random 20) }}`,
)

if warning == "" || !strings.Contains(warning, "calling token display name/IAM policy name were truncated to 64 characters") {
t.Fatalf("expected a truncate warning; received empty string")
}
if len(testUsername) != 64 {
t.Fatalf("expected a username cap at 64 chars; got length: %d", len(testUsername))
}

testUsername, warning, err = genUsername("name1", "policy1", "sts", defaultSTSTemplate)
if strings.Contains(testUsername, "name1") || strings.Contains(testUsername, "policy1") {
t.Fatalf(
"expected sts username to not contain display name or policy name; got %s",
testUsername,
)
}
// STS usernames are capped at 64 characters
if len(testUsername) > 32 {
t.Fatalf(
"expected sts username to be under 32 chars; got %s of length %d",
testUsername,
len(testUsername),
)
type testCase struct {
name string
policy string
userType string
UsernameTemplate string
expectedError string
expectedRegex string
expectedLength int
}

tests := map[string]testCase{
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
"Truncated to 64. No warnings expected": {
name: "name1",
policy: "policy1",
userType: "iam_user",
UsernameTemplate: defaultUserNameTemplate,
expectedError: "",
expectedRegex: `^vault-name1-policy1-[0-9]+-[a-zA-Z0-9]+`,
expectedLength: 64,
},
"Truncated to 32. No warnings expected": {
name: "name1",
policy: "policy1",
userType: "sts",
UsernameTemplate: defaultUserNameTemplate,
expectedError: "",
expectedRegex: `^vault-[0-9]+-[a-zA-Z0-9]+`,
expectedLength: 32,
},
"Too long. Error expected — IAM": {
name: "this---is---a---very---long---name",
policy: "long------policy------name",
userType: "assume_role",
UsernameTemplate: `{{ if (eq .Type "IAM") }}{{ printf "%s-%s-%s-%s" (.DisplayName) (.PolicyName) (unix_time) (random 20) }}{{ end }}`,
expectedError: "the username generated by the template exceeds the IAM username length limits of 64 chars",
expectedRegex: "",
expectedLength: 64,
},
"Too long. Error expected — STS": {
name: "this---is---a---very---long---name",
policy: "long------policy------name",
userType: "sts",
UsernameTemplate: `{{ if (eq .Type "STS") }}{{ printf "%s-%s-%s-%s" (.DisplayName) (.PolicyName) (unix_time) (random 20) }}{{ end }}`,
expectedError: "the username generated by the template exceeds the STS username length limits of 32 chars",
expectedRegex: "",
expectedLength: 32,
},
}

for testDescription, testCase := range tests {
t.Run(testDescription, func(t *testing.T) {
testUsername, err := genUsername(testCase.name, testCase.policy, testCase.userType, testCase.UsernameTemplate)
if err != nil && !strings.Contains(err.Error(), testCase.expectedError) {
t.Fatalf("expected an error %s; instead received %s", testCase.expectedError, err)
}

if err == nil {
require.Regexp(t, testCase.expectedRegex, testUsername)

if len(testUsername) > testCase.expectedLength {
t.Fatalf("expected username to be of length %d, got %d", testCase.expectedLength, len(testUsername))
}
}
})
}
}

Expand Down
3 changes: 3 additions & 0 deletions changelog/12185.txt
@@ -0,0 +1,3 @@
```release-note:improvement
secrets/aws: Add conditional template that allows custom usernames for both STS and IAM cases
```