Skip to content

Commit

Permalink
add aliasmetadata sdk helper and add to aws auth
Browse files Browse the repository at this point in the history
  • Loading branch information
tyrannosaurus-becks committed Apr 20, 2020
1 parent 4d60563 commit c5024cd
Show file tree
Hide file tree
Showing 11 changed files with 1,153 additions and 77 deletions.
46 changes: 39 additions & 7 deletions builtin/credential/aws/path_config_identity.go
Expand Up @@ -5,10 +5,34 @@ import (
"fmt"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/aliasmetadata"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
)

// aliasMetadataFields is a list of the default alias metadata
// added to tokens during login. The default alias type used
// by this back-end is the role ID. Subsequently, the default
// fields included are expected to have a low rate of change
// when the role ID is in use.
var aliasMetadataFields = &aliasmetadata.Fields{
Default: []string{
"account_id",
"auth_type",
},
AvailableToAdd: []string{
"ami_id",
"canonical_arn",
"client_arn",
"client_user_id",
"inferred_aws_region",
"inferred_entity_id",
"inferred_entity_type",
"instance_id",
"region",
},
}

func (b *backend) pathConfigIdentity() *framework.Path {
return &framework.Path{
Pattern: "config/identity$",
Expand All @@ -23,6 +47,7 @@ func (b *backend) pathConfigIdentity() *framework.Path {
Default: identityAliasEC2InstanceID,
Description: fmt.Sprintf("Configure how the AWS auth method generates entity alias when using EC2 auth. Valid values are %q, %q, and %q. Defaults to %q.", identityAliasRoleID, identityAliasEC2InstanceID, identityAliasEC2ImageID, identityAliasRoleID),
},
aliasmetadata.FieldName: aliasmetadata.FieldSchema(aliasMetadataFields),
},

Operations: map[logical.Operation]framework.OperationHandler{
Expand All @@ -45,9 +70,11 @@ func identityConfigEntry(ctx context.Context, s logical.Storage) (*identityConfi
return nil, err
}

var entry identityConfig
entry := &identityConfig{
Handler: aliasmetadata.NewHandler(aliasMetadataFields),
}
if entryRaw != nil {
if err := entryRaw.DecodeJSON(&entry); err != nil {
if err := entryRaw.DecodeJSON(entry); err != nil {
return nil, err
}
}
Expand All @@ -60,7 +87,7 @@ func identityConfigEntry(ctx context.Context, s logical.Storage) (*identityConfi
entry.EC2Alias = identityAliasRoleID
}

return &entry, nil
return entry, nil
}

func pathConfigIdentityRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
Expand All @@ -71,8 +98,9 @@ func pathConfigIdentityRead(ctx context.Context, req *logical.Request, _ *framew

return &logical.Response{
Data: map[string]interface{}{
"iam_alias": config.IAMAlias,
"ec2_alias": config.EC2Alias,
"iam_alias": config.IAMAlias,
"ec2_alias": config.EC2Alias,
aliasmetadata.FieldName: config.GetAliasMetadata(),
},
}, nil
}
Expand Down Expand Up @@ -102,6 +130,9 @@ func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *f
}
config.EC2Alias = ec2Alias
}
if err := config.ParseAliasMetadata(data); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

entry, err := logical.StorageEntryJSON("config/identity", config)
if err != nil {
Expand All @@ -117,8 +148,9 @@ func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *f
}

type identityConfig struct {
IAMAlias string `json:"iam_alias"`
EC2Alias string `json:"ec2_alias"`
IAMAlias string `json:"iam_alias"`
EC2Alias string `json:"ec2_alias"`
aliasmetadata.Handler `json:"alias_metadata_handler"`
}

const identityAliasIAMUniqueID = "unique_id"
Expand Down
36 changes: 20 additions & 16 deletions builtin/credential/aws/path_login.go
Expand Up @@ -845,15 +845,15 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request,
},
Alias: &logical.Alias{
Name: identityAlias,
Metadata: map[string]string{
"instance_id": identityDocParsed.InstanceID,
"region": identityDocParsed.Region,
"account_id": identityDocParsed.AccountID,
"ami_id": identityDocParsed.AmiID,
},
},
}
roleEntry.PopulateTokenAuth(auth)
identityConfigEntry.PopulateDesiredAliasMetadata(auth, map[string]string{
"instance_id": identityDocParsed.InstanceID,
"region": identityDocParsed.Region,
"account_id": identityDocParsed.AccountID,
"ami_id": identityDocParsed.AmiID,
})

resp := &logical.Response{
Auth: auth,
Expand Down Expand Up @@ -1375,19 +1375,23 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request,
DisplayName: entity.FriendlyName,
Alias: &logical.Alias{
Name: identityAlias,
Metadata: map[string]string{
"client_arn": callerID.Arn,
"canonical_arn": entity.canonicalArn(),
"client_user_id": callerUniqueId,
"auth_type": iamAuthType,
"inferred_entity_type": inferredEntityType,
"inferred_entity_id": inferredEntityID,
"inferred_aws_region": roleEntry.InferredAWSRegion,
"account_id": entity.AccountNumber,
},
},
}
roleEntry.PopulateTokenAuth(auth)
if err := identityConfigEntry.PopulateDesiredAliasMetadata(auth, map[string]string{
"client_arn": callerID.Arn,
"canonical_arn": entity.canonicalArn(),
"client_user_id": callerUniqueId,
"auth_type": iamAuthType,
"inferred_entity_type": inferredEntityType,
"inferred_entity_id": inferredEntityID,
"inferred_aws_region": roleEntry.InferredAWSRegion,
"account_id": entity.AccountNumber,
}); err != nil {
if b.Logger().IsWarn() {
b.Logger().Warn(fmt.Sprintf("unable to set alias metadata due to %s", err))
}
}

return &logical.Response{
Auth: auth,
Expand Down
29 changes: 29 additions & 0 deletions builtin/credential/aws/path_login_test.go
Expand Up @@ -11,6 +11,8 @@ import (
"strings"
"testing"

"github.com/hashicorp/vault/sdk/helper/aliasmetadata"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/hashicorp/vault/sdk/logical"
Expand Down Expand Up @@ -214,8 +216,35 @@ func TestBackend_pathLogin_IAMHeaders(t *testing.T) {
t.Fatal(err)
}

// Configure identity.
_, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/identity",
Storage: storage,
Data: map[string]interface{}{
aliasmetadata.FieldName: []string{
"default",
"ami_id",
"canonical_arn",
"client_arn",
"client_user_id",
"inferred_aws_region",
"inferred_entity_id",
"inferred_entity_type",
"instance_id",
"region",
},
"iam_alias": "role_id",
"ec2_alias": "role_id",
},
})
if err != nil {
t.Fatal(err)
}

// create a role entry
roleEntry := &awsRoleEntry{
RoleID: "foo",
Version: currentRoleStorageVersion,
AuthType: iamAuthType,
}
Expand Down
194 changes: 194 additions & 0 deletions sdk/helper/aliasmetadata/alias_metadata.go
@@ -0,0 +1,194 @@
package aliasmetadata

/*
aliasmetadata is a package offering convenience and
standardization when supporting an `alias_metadata`
field in a plugin's configuration. This then controls
what alias metadata is added to an Auth during login.
To see an example of how to add and use it, check out
how these structs and fields are used in the AWS auth
method.
Or, check out its acceptance test in this package to
see its integration points.
*/

import (
"errors"
"fmt"
"strings"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
)

// FieldName is the user-facing name for the field.
const FieldName = "alias_metadata"

// Fields is for configuring a back-end's available
// default and additional fields. These are used for
// providing a verbose field description, and for parsing
// user input.
type Fields struct {
// Default is a list of the default fields that should
// be included if a user sends "default" in their list
// of desired fields. These fields should all have a
// low rate of change because each change can incur a
// write to storage.
Default []string

// AvailableToAdd is a list of fields not included by
// default, that the user may include.
AvailableToAdd []string
}

func (f *Fields) all() []string {
return append(f.Default, f.AvailableToAdd...)
}

// FieldSchema takes the default and additionally available
// fields, and uses them to generate a verbose description
// regarding how to use the "alias_metadata" field.
func FieldSchema(fields *Fields) *framework.FieldSchema {
return &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: description(fields),
DisplayAttrs: &framework.DisplayAttributes{
Name: FieldName,
Value: "default,field1,field2",
},
Default: []string{"default"},
}
}

// NewHandler instantiates a Handler to be embedded in your config.
func NewHandler(fields *Fields) Handler {
return &handler{
fields: fields,
}
}

// Handler is an interface for the helper methods you get on your
// config when you embed the Handler.
type Handler interface {
GetAliasMetadata() []string
ParseAliasMetadata(data *framework.FieldData) error
PopulateDesiredAliasMetadata(auth *logical.Auth, fieldValues map[string]string) error
}

type handler struct {
// AliasMetadata is an explicit list of all the user's configured
// fields that are being added to alias metadata. It will never
// include the "default" parameter, and instead includes the actual
// fields behind "default", if selected. If it has never been set,
// the pointer will be nil.
AliasMetadata *[]string `json:"alias_metadata"`

// fields is a list of the configured default and available
// fields. It's intentionally not jsonified.
fields *Fields
}

// GetAliasMetadata is intended to be used on config reads.
// It gets an explicit list of all the user's configured
// fields that are being added to alias metadata. It will never
// include the "default" parameter, and instead includes the actual
// fields behind "default", if selected.
func (h *handler) GetAliasMetadata() []string {
if h.AliasMetadata == nil {
return h.fields.Default
}
return *h.AliasMetadata
}

// ParseAliasMetadata is intended to be used on config create/update.
// It takes a user's selected fields (or lack thereof),
// converts it to a list of explicit fields, and adds it to the handler
// for later storage.
func (h *handler) ParseAliasMetadata(data *framework.FieldData) error {
userProvided, ok := data.GetOk(FieldName)
if !ok {
// Nothing further to do here.
return nil
}

// uniqueFields protects against weird edge cases like if
// a user provided "default,field1,field2,default".
uniqueFields := make(map[string]bool)
for _, field := range userProvided.([]string) {
if field == "default" {
// Add the fields that "default" represents, rather
// than the explicit field.
for _, dfltField := range h.fields.Default {
uniqueFields[dfltField] = true
}
} else {
// Make sure they've sent a supported field so we can
// error early if not.
if !strutil.StrListContains(h.fields.all(), field) {
return fmt.Errorf("%q is not an available field, please select from: %s", field, strings.Join(h.fields.all(), ", "))
}
uniqueFields[field] = true
}
}
// Attach the fields we've received so they'll be stored.
aliasMetadata := make([]string, len(uniqueFields))
i := 0
for fieldName := range uniqueFields {
aliasMetadata[i] = fieldName
i++
}
// Fulfilling the pointer here flags that the user has made
// an explicit selection so we shouldn't just fall back to
// our defaults.
h.AliasMetadata = &aliasMetadata
return nil
}

// PopulateDesiredAliasMetadata is intended to be used during login
// just before returning an auth.
// It takes the available alias metadata and,
// if the auth should have it, adds it to the auth's alias metadata.
func (h *handler) PopulateDesiredAliasMetadata(auth *logical.Auth, available map[string]string) error {
if auth == nil {
return errors.New("auth is nil")
}
if auth.Alias == nil {
return errors.New("auth alias is nil")
}
if auth.Alias.Name == "" {
// We need the caller to set the alias name or there will
// be nothing for these fields to operate upon.
return errors.New("auth alias name must be set")
}
if auth.Alias.Metadata == nil {
auth.Alias.Metadata = make(map[string]string)
}
fieldsToInclude := h.fields.Default
if h.AliasMetadata != nil {
fieldsToInclude = *h.AliasMetadata
}
for availableField, itsValue := range available {
if strutil.StrListContains(fieldsToInclude, availableField) {
auth.Alias.Metadata[availableField] = itsValue
}
}
return nil
}

func description(fields *Fields) string {
desc := "The metadata to include on the aliases generated by this plugin."
if len(fields.Default) > 0 {
desc += fmt.Sprintf(" When set to 'default', includes: %s.", strings.Join(fields.Default, ", "))
}
if len(fields.AvailableToAdd) > 0 {
desc += fmt.Sprintf(" These fields are available to add: %s.", strings.Join(fields.AvailableToAdd, ", "))
}
desc += " Not editing this field means the 'default' fields are included." +
" Explicitly setting this field to empty overrides the 'default' and means no alias metadata will be included." +
" Add fields by sending, 'default,field1,field2'." +
" We advise only including fields that change rarely because each change triggers a storage write."
return desc
}

0 comments on commit c5024cd

Please sign in to comment.