mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	 e14d32c528
			
		
	
	e14d32c528
	
	
	
		
			
			* update genUsername to cap STS usernames at 64 chars * add changelog * refactor tests into t.Run block * patch: remove warningExpected bool and include expected string * patch: revert sts to cap at 32 chars and add assume_role case in genUsername * update changelog * update genUsername to return error if username generated exceeds length limits * update changelog * add conditional default username template to provide custom STS usernames * update changelog * include test for failing STS length case * update comments for more clarity
		
			
				
	
	
		
			513 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			513 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package aws
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"regexp"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/hashicorp/go-secure-stdlib/awsutil"
 | |
| 	"github.com/hashicorp/vault/sdk/framework"
 | |
| 	"github.com/hashicorp/vault/sdk/helper/template"
 | |
| 	"github.com/hashicorp/vault/sdk/logical"
 | |
| 
 | |
| 	"github.com/aws/aws-sdk-go/aws"
 | |
| 	"github.com/aws/aws-sdk-go/service/iam"
 | |
| 	"github.com/aws/aws-sdk-go/service/sts"
 | |
| 	"github.com/hashicorp/errwrap"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	secretAccessKeyType = "access_keys"
 | |
| 	storageKey          = "config/root"
 | |
| )
 | |
| 
 | |
| func secretAccessKeys(b *backend) *framework.Secret {
 | |
| 	return &framework.Secret{
 | |
| 		Type: secretAccessKeyType,
 | |
| 		Fields: map[string]*framework.FieldSchema{
 | |
| 			"access_key": {
 | |
| 				Type:        framework.TypeString,
 | |
| 				Description: "Access Key",
 | |
| 			},
 | |
| 
 | |
| 			"secret_key": {
 | |
| 				Type:        framework.TypeString,
 | |
| 				Description: "Secret Key",
 | |
| 			},
 | |
| 			"security_token": {
 | |
| 				Type:        framework.TypeString,
 | |
| 				Description: "Security Token",
 | |
| 			},
 | |
| 		},
 | |
| 
 | |
| 		Renew:  b.secretAccessKeysRenew,
 | |
| 		Revoke: b.secretAccessKeysRevoke,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func genUsername(displayName, policyName, userType, usernameTemplate string) (ret string, err error) {
 | |
| 	switch userType {
 | |
| 	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)
 | |
| 		}
 | |
| 
 | |
| 		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)
 | |
| 		}
 | |
| 		// To prevent a custom template from exceeding IAM length limits
 | |
| 		if len(ret) > 64 {
 | |
| 			return "", fmt.Errorf("the username generated by the template exceeds the IAM username length limits of 64 chars")
 | |
| 		}
 | |
| 	case "sts":
 | |
| 		up, err := template.NewTemplate(template.Template(usernameTemplate))
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("unable to initialize username template: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		um := UsernameMetadata{
 | |
| 			Type: "STS",
 | |
| 		}
 | |
| 		ret, err = up.Generate(um)
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("failed to generate username: %w", err)
 | |
| 		}
 | |
| 		// To prevent a custom template from exceeding STS length limits
 | |
| 		if len(ret) > 32 {
 | |
| 			return "", fmt.Errorf("the username generated by the template exceeds the STS username length limits of 32 chars")
 | |
| 		}
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func (b *backend) getFederationToken(ctx context.Context, s logical.Storage,
 | |
| 	displayName, policyName, policy string, policyARNs []string,
 | |
| 	iamGroups []string, lifeTimeInSeconds int64) (*logical.Response, error) {
 | |
| 
 | |
| 	groupPolicies, groupPolicyARNs, err := b.getGroupPolicies(ctx, s, iamGroups)
 | |
| 	if err != nil {
 | |
| 		return logical.ErrorResponse(err.Error()), nil
 | |
| 	}
 | |
| 	if groupPolicies != nil {
 | |
| 		groupPolicies = append(groupPolicies, policy)
 | |
| 		policy, err = combinePolicyDocuments(groupPolicies...)
 | |
| 		if err != nil {
 | |
| 			return logical.ErrorResponse(err.Error()), nil
 | |
| 		}
 | |
| 	}
 | |
| 	if len(groupPolicyARNs) > 0 {
 | |
| 		policyARNs = append(policyARNs, groupPolicyARNs...)
 | |
| 	}
 | |
| 
 | |
| 	stsClient, err := b.clientSTS(ctx, s)
 | |
| 	if err != nil {
 | |
| 		return logical.ErrorResponse(err.Error()), nil
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	getTokenInput := &sts.GetFederationTokenInput{
 | |
| 		Name:            aws.String(username),
 | |
| 		DurationSeconds: &lifeTimeInSeconds,
 | |
| 	}
 | |
| 	if len(policy) > 0 {
 | |
| 		getTokenInput.Policy = aws.String(policy)
 | |
| 	}
 | |
| 	if len(policyARNs) > 0 {
 | |
| 		getTokenInput.PolicyArns = convertPolicyARNs(policyARNs)
 | |
| 	}
 | |
| 
 | |
| 	// If neither a policy document nor policy ARNs are specified, then GetFederationToken will
 | |
| 	// return credentials equivalent to that of the Vault server itself. We probably don't want
 | |
| 	// that by default; the behavior can be explicitly opted in to by associating the Vault role
 | |
| 	// with a policy ARN or document that allows the appropriate permissions.
 | |
| 	if policy == "" && len(policyARNs) == 0 {
 | |
| 		return logical.ErrorResponse("must specify at least one of policy_arns or policy_document with %s credential_type", federationTokenCred), nil
 | |
| 	}
 | |
| 
 | |
| 	tokenResp, err := stsClient.GetFederationToken(getTokenInput)
 | |
| 	if err != nil {
 | |
| 		return logical.ErrorResponse("Error generating STS keys: %s", err), awsutil.CheckAWSError(err)
 | |
| 	}
 | |
| 
 | |
| 	resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
 | |
| 		"access_key":     *tokenResp.Credentials.AccessKeyId,
 | |
| 		"secret_key":     *tokenResp.Credentials.SecretAccessKey,
 | |
| 		"security_token": *tokenResp.Credentials.SessionToken,
 | |
| 	}, map[string]interface{}{
 | |
| 		"username": username,
 | |
| 		"policy":   policy,
 | |
| 		"is_sts":   true,
 | |
| 	})
 | |
| 
 | |
| 	// Set the secret TTL to appropriately match the expiration of the token
 | |
| 	resp.Secret.TTL = tokenResp.Credentials.Expiration.Sub(time.Now())
 | |
| 
 | |
| 	// STS are purposefully short-lived and aren't renewable
 | |
| 	resp.Secret.Renewable = false
 | |
| 
 | |
| 	return resp, nil
 | |
| }
 | |
| 
 | |
| func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
 | |
| 	displayName, roleName, roleArn, policy string, policyARNs []string,
 | |
| 	iamGroups []string, lifeTimeInSeconds int64, roleSessionName string) (*logical.Response, error) {
 | |
| 
 | |
| 	// grab any IAM group policies associated with the vault role, both inline
 | |
| 	// and managed
 | |
| 	groupPolicies, groupPolicyARNs, err := b.getGroupPolicies(ctx, s, iamGroups)
 | |
| 	if err != nil {
 | |
| 		return logical.ErrorResponse(err.Error()), nil
 | |
| 	}
 | |
| 	if len(groupPolicies) > 0 {
 | |
| 		groupPolicies = append(groupPolicies, policy)
 | |
| 		policy, err = combinePolicyDocuments(groupPolicies...)
 | |
| 		if err != nil {
 | |
| 			return logical.ErrorResponse(err.Error()), nil
 | |
| 		}
 | |
| 	}
 | |
| 	if len(groupPolicyARNs) > 0 {
 | |
| 		policyARNs = append(policyARNs, groupPolicyARNs...)
 | |
| 	}
 | |
| 
 | |
| 	stsClient, err := b.clientSTS(ctx, s)
 | |
| 	if err != nil {
 | |
| 		return logical.ErrorResponse(err.Error()), nil
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	var roleSessionNameError error
 | |
| 	if roleSessionName == "" {
 | |
| 		roleSessionName, roleSessionNameError = genUsername(displayName, roleName, "assume_role", usernameTemplate)
 | |
| 		// Send a 400 to Framework.OperationFunc Handler
 | |
| 		if roleSessionNameError != nil {
 | |
| 			return nil, roleSessionNameError
 | |
| 		}
 | |
| 	} else {
 | |
| 		roleSessionName = normalizeDisplayName(roleSessionName)
 | |
| 	}
 | |
| 
 | |
| 	assumeRoleInput := &sts.AssumeRoleInput{
 | |
| 		RoleSessionName: aws.String(roleSessionName),
 | |
| 		RoleArn:         aws.String(roleArn),
 | |
| 		DurationSeconds: &lifeTimeInSeconds,
 | |
| 	}
 | |
| 	if policy != "" {
 | |
| 		assumeRoleInput.SetPolicy(policy)
 | |
| 	}
 | |
| 	if len(policyARNs) > 0 {
 | |
| 		assumeRoleInput.SetPolicyArns(convertPolicyARNs(policyARNs))
 | |
| 	}
 | |
| 	tokenResp, err := stsClient.AssumeRole(assumeRoleInput)
 | |
| 	if err != nil {
 | |
| 		return logical.ErrorResponse("Error assuming role: %s", err), awsutil.CheckAWSError(err)
 | |
| 	}
 | |
| 
 | |
| 	resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
 | |
| 		"access_key":     *tokenResp.Credentials.AccessKeyId,
 | |
| 		"secret_key":     *tokenResp.Credentials.SecretAccessKey,
 | |
| 		"security_token": *tokenResp.Credentials.SessionToken,
 | |
| 		"arn":            *tokenResp.AssumedRoleUser.Arn,
 | |
| 	}, map[string]interface{}{
 | |
| 		"username": roleSessionName,
 | |
| 		"policy":   roleArn,
 | |
| 		"is_sts":   true,
 | |
| 	})
 | |
| 
 | |
| 	// Set the secret TTL to appropriately match the expiration of the token
 | |
| 	resp.Secret.TTL = tokenResp.Credentials.Expiration.Sub(time.Now())
 | |
| 
 | |
| 	// STS are purposefully short-lived and aren't renewable
 | |
| 	resp.Secret.Renewable = false
 | |
| 
 | |
| 	return resp, nil
 | |
| }
 | |
| 
 | |
| func readConfig(ctx context.Context, storage logical.Storage) (rootConfig, error) {
 | |
| 	entry, err := storage.Get(ctx, storageKey)
 | |
| 	if err != nil {
 | |
| 		return rootConfig{}, err
 | |
| 	}
 | |
| 	if entry == nil {
 | |
| 		return rootConfig{}, nil
 | |
| 	}
 | |
| 
 | |
| 	var connConfig rootConfig
 | |
| 	if err := entry.DecodeJSON(&connConfig); err != nil {
 | |
| 		return rootConfig{}, err
 | |
| 	}
 | |
| 	return connConfig, nil
 | |
| }
 | |
| 
 | |
| func (b *backend) secretAccessKeysCreate(
 | |
| 	ctx context.Context,
 | |
| 	s logical.Storage,
 | |
| 	displayName, policyName string,
 | |
| 	role *awsRoleEntry) (*logical.Response, error) {
 | |
| 	iamClient, err := b.clientIAM(ctx, s)
 | |
| 	if err != nil {
 | |
| 		return logical.ErrorResponse(err.Error()), nil
 | |
| 	}
 | |
| 
 | |
| 	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, "iam_user", usernameTemplate)
 | |
| 	// Send a 400 to Framework.OperationFunc Handler
 | |
| 	if usernameError != nil {
 | |
| 		return nil, usernameError
 | |
| 	}
 | |
| 
 | |
| 	// Write to the WAL that this user will be created. We do this before
 | |
| 	// the user is created because if switch the order then the WAL put
 | |
| 	// can fail, which would put us in an awkward position: we have a user
 | |
| 	// we need to rollback but can't put the WAL entry to do the rollback.
 | |
| 	walID, err := framework.PutWAL(ctx, s, "user", &walUser{
 | |
| 		UserName: username,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error writing WAL entry: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	userPath := role.UserPath
 | |
| 	if userPath == "" {
 | |
| 		userPath = "/"
 | |
| 	}
 | |
| 
 | |
| 	createUserRequest := &iam.CreateUserInput{
 | |
| 		UserName: aws.String(username),
 | |
| 		Path:     aws.String(userPath),
 | |
| 	}
 | |
| 	if role.PermissionsBoundaryARN != "" {
 | |
| 		createUserRequest.PermissionsBoundary = aws.String(role.PermissionsBoundaryARN)
 | |
| 	}
 | |
| 
 | |
| 	// Create the user
 | |
| 	_, err = iamClient.CreateUser(createUserRequest)
 | |
| 	if err != nil {
 | |
| 		if walErr := framework.DeleteWAL(ctx, s, walID); walErr != nil {
 | |
| 			iamErr := fmt.Errorf("error creating IAM user: %w", err)
 | |
| 			return nil, errwrap.Wrap(fmt.Errorf("failed to delete WAL entry: %w", walErr), iamErr)
 | |
| 		}
 | |
| 		return logical.ErrorResponse("Error creating IAM user: %s", err), awsutil.CheckAWSError(err)
 | |
| 	}
 | |
| 
 | |
| 	for _, arn := range role.PolicyArns {
 | |
| 		// Attach existing policy against user
 | |
| 		_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
 | |
| 			UserName:  aws.String(username),
 | |
| 			PolicyArn: aws.String(arn),
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return logical.ErrorResponse("Error attaching user policy: %s", err), awsutil.CheckAWSError(err)
 | |
| 		}
 | |
| 
 | |
| 	}
 | |
| 	if role.PolicyDocument != "" {
 | |
| 		// Add new inline user policy against user
 | |
| 		_, err = iamClient.PutUserPolicy(&iam.PutUserPolicyInput{
 | |
| 			UserName:       aws.String(username),
 | |
| 			PolicyName:     aws.String(policyName),
 | |
| 			PolicyDocument: aws.String(role.PolicyDocument),
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return logical.ErrorResponse("Error putting user policy: %s", err), awsutil.CheckAWSError(err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, group := range role.IAMGroups {
 | |
| 		// Add user to IAM groups
 | |
| 		_, err = iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
 | |
| 			UserName:  aws.String(username),
 | |
| 			GroupName: aws.String(group),
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return logical.ErrorResponse("Error adding user to group: %s", err), awsutil.CheckAWSError(err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	var tags []*iam.Tag
 | |
| 	for key, value := range role.IAMTags {
 | |
| 		// This assignment needs to be done in order to create unique addresses for
 | |
| 		// these variables. Without doing so, all the tags will be copies of the last
 | |
| 		// tag listed in the role.
 | |
| 		k, v := key, value
 | |
| 		tags = append(tags, &iam.Tag{Key: &k, Value: &v})
 | |
| 	}
 | |
| 
 | |
| 	if len(tags) > 0 {
 | |
| 		_, err = iamClient.TagUser(&iam.TagUserInput{
 | |
| 			Tags:     tags,
 | |
| 			UserName: &username,
 | |
| 		})
 | |
| 
 | |
| 		if err != nil {
 | |
| 			return logical.ErrorResponse("Error adding tags to user: %s", err), awsutil.CheckAWSError(err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Create the keys
 | |
| 	keyResp, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{
 | |
| 		UserName: aws.String(username),
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return logical.ErrorResponse("Error creating access keys: %s", err), awsutil.CheckAWSError(err)
 | |
| 	}
 | |
| 
 | |
| 	// Remove the WAL entry, we succeeded! If we fail, we don't return
 | |
| 	// the secret because it'll get rolled back anyways, so we have to return
 | |
| 	// an error here.
 | |
| 	if err := framework.DeleteWAL(ctx, s, walID); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to commit WAL entry: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Return the info!
 | |
| 	resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
 | |
| 		"access_key":     *keyResp.AccessKey.AccessKeyId,
 | |
| 		"secret_key":     *keyResp.AccessKey.SecretAccessKey,
 | |
| 		"security_token": nil,
 | |
| 	}, map[string]interface{}{
 | |
| 		"username": username,
 | |
| 		"policy":   role,
 | |
| 		"is_sts":   false,
 | |
| 	})
 | |
| 
 | |
| 	lease, err := b.Lease(ctx, s)
 | |
| 	if err != nil || lease == nil {
 | |
| 		lease = &configLease{}
 | |
| 	}
 | |
| 
 | |
| 	resp.Secret.TTL = lease.Lease
 | |
| 	resp.Secret.MaxTTL = lease.LeaseMax
 | |
| 
 | |
| 	return resp, nil
 | |
| }
 | |
| 
 | |
| func (b *backend) secretAccessKeysRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
 | |
| 	// STS already has a lifetime, and we don't support renewing it
 | |
| 	isSTSRaw, ok := req.Secret.InternalData["is_sts"]
 | |
| 	if ok {
 | |
| 		isSTS, ok := isSTSRaw.(bool)
 | |
| 		if ok {
 | |
| 			if isSTS {
 | |
| 				return nil, nil
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	lease, err := b.Lease(ctx, req.Storage)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if lease == nil {
 | |
| 		lease = &configLease{}
 | |
| 	}
 | |
| 
 | |
| 	resp := &logical.Response{Secret: req.Secret}
 | |
| 	resp.Secret.TTL = lease.Lease
 | |
| 	resp.Secret.MaxTTL = lease.LeaseMax
 | |
| 	return resp, nil
 | |
| }
 | |
| 
 | |
| func (b *backend) secretAccessKeysRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
 | |
| 	// STS cleans up after itself so we can skip this if is_sts internal data
 | |
| 	// element set to true. If is_sts is not set, assumes old version
 | |
| 	// and defaults to the IAM approach.
 | |
| 	isSTSRaw, ok := req.Secret.InternalData["is_sts"]
 | |
| 	if ok {
 | |
| 		isSTS, ok := isSTSRaw.(bool)
 | |
| 		if ok {
 | |
| 			if isSTS {
 | |
| 				return nil, nil
 | |
| 			}
 | |
| 		} else {
 | |
| 			return nil, fmt.Errorf("secret has is_sts but value could not be understood")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Get the username from the internal data
 | |
| 	usernameRaw, ok := req.Secret.InternalData["username"]
 | |
| 	if !ok {
 | |
| 		return nil, fmt.Errorf("secret is missing username internal data")
 | |
| 	}
 | |
| 	username, ok := usernameRaw.(string)
 | |
| 	if !ok {
 | |
| 		return nil, fmt.Errorf("secret is missing username internal data")
 | |
| 	}
 | |
| 
 | |
| 	// Use the user rollback mechanism to delete this user
 | |
| 	err := b.pathUserRollback(ctx, req, "user", map[string]interface{}{
 | |
| 		"username": username,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return nil, nil
 | |
| }
 | |
| 
 | |
| func normalizeDisplayName(displayName string) string {
 | |
| 	re := regexp.MustCompile("[^a-zA-Z0-9+=,.@_-]")
 | |
| 	return re.ReplaceAllString(displayName, "_")
 | |
| }
 | |
| 
 | |
| func convertPolicyARNs(policyARNs []string) []*sts.PolicyDescriptorType {
 | |
| 	size := len(policyARNs)
 | |
| 	retval := make([]*sts.PolicyDescriptorType, size, size)
 | |
| 	for i, arn := range policyARNs {
 | |
| 		retval[i] = &sts.PolicyDescriptorType{
 | |
| 			Arn: aws.String(arn),
 | |
| 		}
 | |
| 	}
 | |
| 	return retval
 | |
| }
 | |
| 
 | |
| type UsernameMetadata struct {
 | |
| 	Type        string
 | |
| 	DisplayName string
 | |
| 	PolicyName  string
 | |
| }
 |