diff --git a/builtin/credential/aws/path_config_identity.go b/builtin/credential/aws/path_config_identity.go index 28e5aa11c6dc2..581aa0ac91e52 100644 --- a/builtin/credential/aws/path_config_identity.go +++ b/builtin/credential/aws/path_config_identity.go @@ -5,10 +5,52 @@ import ( "fmt" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/authmetadata" "github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/logical" ) +var ( + // iamAuthMetadataFields is a list of the default auth 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. + iamAuthMetadataFields = &authmetadata.Fields{ + FieldName: "iam_metadata", + Default: []string{ + "account_id", + "auth_type", + }, + AvailableToAdd: []string{ + "canonical_arn", + "client_arn", + "client_user_id", + "inferred_aws_region", + "inferred_entity_id", + "inferred_entity_type", + }, + } + + // ec2AuthMetadataFields is a list of the default auth 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. + ec2AuthMetadataFields = &authmetadata.Fields{ + FieldName: "ec2_metadata", + Default: []string{ + "account_id", + "auth_type", + }, + AvailableToAdd: []string{ + "ami_id", + "instance_id", + "region", + }, + } +) + func (b *backend) pathConfigIdentity() *framework.Path { return &framework.Path{ Pattern: "config/identity$", @@ -18,11 +60,13 @@ func (b *backend) pathConfigIdentity() *framework.Path { Default: identityAliasIAMUniqueID, Description: fmt.Sprintf("Configure how the AWS auth method generates entity aliases when using IAM auth. Valid values are %q, %q, and %q. Defaults to %q.", identityAliasRoleID, identityAliasIAMUniqueID, identityAliasIAMFullArn, identityAliasRoleID), }, + iamAuthMetadataFields.FieldName: authmetadata.FieldSchema(iamAuthMetadataFields), "ec2_alias": { Type: framework.TypeString, 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), }, + ec2AuthMetadataFields.FieldName: authmetadata.FieldSchema(ec2AuthMetadataFields), }, Operations: map[logical.Operation]framework.OperationHandler{ @@ -45,9 +89,12 @@ func identityConfigEntry(ctx context.Context, s logical.Storage) (*identityConfi return nil, err } - var entry identityConfig + entry := &identityConfig{ + IAMAuthMetadataHandler: authmetadata.NewHandler(iamAuthMetadataFields), + EC2AuthMetadataHandler: authmetadata.NewHandler(ec2AuthMetadataFields), + } if entryRaw != nil { - if err := entryRaw.DecodeJSON(&entry); err != nil { + if err := entryRaw.DecodeJSON(entry); err != nil { return nil, err } } @@ -60,7 +107,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) { @@ -71,8 +118,10 @@ 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, + iamAuthMetadataFields.FieldName: config.IAMAuthMetadataHandler.AuthMetadata(), + "ec2_alias": config.EC2Alias, + ec2AuthMetadataFields.FieldName: config.EC2AuthMetadataHandler.AuthMetadata(), }, }, nil } @@ -102,6 +151,12 @@ func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *f } config.EC2Alias = ec2Alias } + if err := config.IAMAuthMetadataHandler.ParseAuthMetadata(data); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + if err := config.EC2AuthMetadataHandler.ParseAuthMetadata(data); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } entry, err := logical.StorageEntryJSON("config/identity", config) if err != nil { @@ -117,8 +172,10 @@ 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"` + IAMAuthMetadataHandler *authmetadata.Handler `json:"iam_auth_metadata_handler"` + EC2Alias string `json:"ec2_alias"` + EC2AuthMetadataHandler *authmetadata.Handler `json:"ec2_auth_metadata_handler"` } const identityAliasIAMUniqueID = "unique_id" diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 6b8fe95d6a0e6..de642ed6f7596 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -830,24 +830,23 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request, auth := &logical.Auth{ Metadata: map[string]string{ - "instance_id": identityDocParsed.InstanceID, - "region": identityDocParsed.Region, - "account_id": identityDocParsed.AccountID, "role_tag_max_ttl": rTagMaxTTL.String(), "role": roleName, - "ami_id": identityDocParsed.AmiID, }, 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) + if err := identityConfigEntry.EC2AuthMetadataHandler.PopulateDesiredMetadata(auth, map[string]string{ + "instance_id": identityDocParsed.InstanceID, + "region": identityDocParsed.Region, + "account_id": identityDocParsed.AccountID, + "ami_id": identityDocParsed.AmiID, + "auth_type": ec2AuthType, + }); err != nil { + b.Logger().Warn("unable to set alias metadata", "err", err) + } resp := &logical.Response{ Auth: auth, @@ -1348,15 +1347,7 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, auth := &logical.Auth{ 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, - "role_id": roleEntry.RoleID, + "role_id": roleEntry.RoleID, }, InternalData: map[string]interface{}{ "role_name": roleName, @@ -1365,19 +1356,21 @@ 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.IAMAuthMetadataHandler.PopulateDesiredMetadata(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 { + b.Logger().Warn(fmt.Sprintf("unable to set alias metadata due to %s", err)) + } return &logical.Response{ Auth: auth, diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index 59d120d53d418..ef5a5aacb48a6 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -214,8 +214,39 @@ 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{}{ + "iam_alias": "role_id", + "iam_metadata": []string{ + "account_id", + "auth_type", + "canonical_arn", + "client_arn", + "client_user_id", + "inferred_aws_region", + "inferred_entity_id", + "inferred_entity_type", + }, + "ec2_alias": "role_id", + "ec2_metadata": []string{ + "account_id", + "ami_id", + "instance_id", + "region", + }, + }, + }) + if err != nil { + t.Fatal(err) + } + // create a role entry roleEntry := &awsRoleEntry{ + RoleID: "foo", Version: currentRoleStorageVersion, AuthType: iamAuthType, } @@ -232,16 +263,12 @@ func TestBackend_pathLogin_IAMHeaders(t *testing.T) { t.Fatal(err) } - expectedAliasMetadata := map[string]string{ + expectedAuthMetadata := map[string]string{ "account_id": "123456789012", "auth_type": "iam", "canonical_arn": "arn:aws:iam::123456789012:user/valid-role", "client_arn": "arn:aws:iam::123456789012:user/valid-role", "client_user_id": "ASOMETHINGSOMETHINGSOMETHING", - // Note there is no inferred entity, so these fields should be empty - "inferred_aws_region": "", - "inferred_entity_id": "", - "inferred_entity_type": "", } // expected errors for certain tests @@ -317,6 +344,142 @@ func TestBackend_pathLogin_IAMHeaders(t *testing.T) { }, } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + if tc.Header != nil { + loginData["iam_request_headers"] = tc.Header + } + + loginRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: loginData, + Connection: &logical.Connection{}, + } + + resp, err := b.HandleRequest(context.Background(), loginRequest) + if err != nil || resp == nil || resp.IsError() { + if tc.ExpectErr != nil && tc.ExpectErr.Error() == resp.Error().Error() { + return + } + t.Errorf("un expected failed login:\nresp: %#v\n\nerr: %v", resp, err) + } + + if !reflect.DeepEqual(expectedAuthMetadata, resp.Auth.Alias.Metadata) { + t.Errorf("expected metadata (%#v) to match (%#v)", expectedAuthMetadata, resp.Auth.Alias.Metadata) + } + }) + } +} + +func TestBackend_defaultAliasMetadata(t *testing.T) { + storage := &logical.InmemStorage{} + config := logical.TestBackendConfig() + config.StorageView = storage + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + + err = b.Setup(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + // sets up a test server to stand in for STS service + ts := setupIAMTestServer() + defer ts.Close() + + clientConfigData := map[string]interface{}{ + "iam_server_id_header_value": testVaultHeaderValue, + "sts_endpoint": ts.URL, + } + clientRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/client", + Storage: storage, + Data: clientConfigData, + } + _, err = b.HandleRequest(context.Background(), clientRequest) + if err != nil { + t.Fatal(err) + } + + // Configure identity. + _, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/identity", + Storage: storage, + Data: map[string]interface{}{ + "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, + } + + if err := b.setRole(context.Background(), storage, testValidRoleName, roleEntry); err != nil { + t.Fatalf("failed to set entry: %s", err) + } + + // create a baseline loginData map structure, including iam_request_headers + // already base64encoded. This is the "Default" loginData used for all tests. + // Each sub test can override the map's iam_request_headers entry + loginData, err := defaultLoginData() + if err != nil { + t.Fatal(err) + } + + expectedAliasMetadata := map[string]string{ + "account_id": "123456789012", + "auth_type": "iam", + } + + testCases := []struct { + Name string + Header interface{} + ExpectErr error + }{ + { + Name: "Default", + }, + { + Name: "Map-complete", + Header: map[string]interface{}{ + "Content-Length": "43", + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + "User-Agent": "aws-sdk-go/1.14.24 (go1.11; darwin; amd64)", + "X-Amz-Date": "20180910T203328Z", + "X-Vault-Aws-Iam-Server-Id": "VaultAcceptanceTesting", + "Authorization": "AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180910/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=cdef5819b2e97f1ff0f3e898fd2621aa03af00a4ec3e019122c20e5482534bf4", + }, + }, + { + Name: "JSON-complete", + Header: `{ + "Content-Length":"43", + "Content-Type":"application/x-www-form-urlencoded; charset=utf-8", + "User-Agent":"aws-sdk-go/1.14.24 (go1.11; darwin; amd64)", + "X-Amz-Date":"20180910T203328Z", + "X-Vault-Aws-Iam-Server-Id": "VaultAcceptanceTesting", + "Authorization":"AWS4-HMAC-SHA256 Credential=AKIAJPQ466AIIQW4LPSQ/20180910/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=cdef5819b2e97f1ff0f3e898fd2621aa03af00a4ec3e019122c20e5482534bf4" + }`, + }, + { + Name: "Base64-complete", + Header: base64Complete(), + }, + } + for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { if tc.Header != nil { diff --git a/sdk/helper/authmetadata/auth_metadata.go b/sdk/helper/authmetadata/auth_metadata.go new file mode 100644 index 0000000000000..c1e4e93d59369 --- /dev/null +++ b/sdk/helper/authmetadata/auth_metadata.go @@ -0,0 +1,200 @@ +package authmetadata + +/* + authmetadata is a package offering convenience and + standardization when supporting an `auth_metadata` + field in a plugin's configuration. This then controls + what 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 ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/strutil" + "github.com/hashicorp/vault/sdk/logical" +) + +// 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 { + // The field name as it'll be reflected in the user-facing + // schema. + FieldName string + + // 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 "auth_metadata" field. +func FieldSchema(fields *Fields) *framework.FieldSchema { + return &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: description(fields), + DisplayAttrs: &framework.DisplayAttributes{ + Name: fields.FieldName, + Value: "field1,field2", + }, + Default: []string{"default"}, + } +} + +func NewHandler(fields *Fields) *Handler { + return &Handler{ + fields: fields, + } +} + +type Handler struct { + // authMetadata is an explicit list of all the user's configured + // fields that are being added to auth metadata. If it is set to + // default or unconfigured, it will be nil. Otherwise, it will + // hold the explicit fields set by the user. + authMetadata []string + + // fields is a list of the configured default and available + // fields. + fields *Fields +} + +// AuthMetadata 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 auth metadata. +func (h *Handler) AuthMetadata() []string { + if h.authMetadata == nil { + return h.fields.Default + } + return h.authMetadata +} + +// ParseAuthMetadata 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) ParseAuthMetadata(data *framework.FieldData) error { + userProvidedRaw, ok := data.GetOk(h.fields.FieldName) + if !ok { + // Nothing further to do here. + return nil + } + userProvided, ok := userProvidedRaw.([]string) + if !ok { + return fmt.Errorf("%s is an unexpected type of %T", userProvidedRaw, userProvidedRaw) + } + userProvided = strutil.RemoveDuplicates(userProvided, true) + + // If the only field the user has chosen was the default field, + // we don't store anything so we won't have to do a storage + // migration if the default changes. + if len(userProvided) == 1 && userProvided[0] == "default" { + h.authMetadata = nil + return nil + } + + // Validate and store the input. + if strutil.StrListContains(userProvided, "default") { + return fmt.Errorf("%q contains default - default can't be used in combination with other fields", + userProvided) + } + if !strutil.StrListSubset(h.fields.all(), userProvided) { + return fmt.Errorf("%q contains an unavailable field, please select from %q", + strings.Join(userProvided, ", "), strings.Join(h.fields.all(), ", ")) + } + h.authMetadata = userProvided + return nil +} + +// PopulateDesiredMetadata is intended to be used during login +// just before returning an auth. +// It takes the available auth metadata and, +// if the auth should have it, adds it to the auth's metadata. +func (h *Handler) PopulateDesiredMetadata(auth *logical.Auth, available map[string]string) error { + if auth == nil { + return errors.New("auth is nil") + } + if auth.Metadata == nil { + auth.Metadata = make(map[string]string) + } + if auth.Alias == nil { + auth.Alias = &logical.Alias{} + } + if auth.Alias.Metadata == nil { + auth.Alias.Metadata = make(map[string]string) + } + fieldsToInclude := h.fields.Default + if h.authMetadata != nil { + fieldsToInclude = h.authMetadata + } + for availableField, itsValue := range available { + if itsValue == "" { + // Don't bother setting fields for which there is no value. + continue + } + if strutil.StrListContains(fieldsToInclude, availableField) { + auth.Metadata[availableField] = itsValue + auth.Alias.Metadata[availableField] = itsValue + } + } + return nil +} + +func (h *Handler) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + AuthMetadata []string `json:"auth_metadata"` + }{ + AuthMetadata: h.authMetadata, + }) +} + +func (h *Handler) UnmarshalJSON(data []byte) error { + jsonable := &struct { + AuthMetadata []string `json:"auth_metadata"` + }{ + AuthMetadata: h.authMetadata, + } + if err := json.Unmarshal(data, jsonable); err != nil { + return err + } + h.authMetadata = jsonable.AuthMetadata + return nil +} + +func description(fields *Fields) string { + desc := "The metadata to include on the aliases and audit logs 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 metadata will be included." + + " If not using 'default', explicit fields must be sent like: 'field1,field2'." + return desc +} diff --git a/sdk/helper/authmetadata/auth_metadata_acc_test.go b/sdk/helper/authmetadata/auth_metadata_acc_test.go new file mode 100644 index 0000000000000..39888c69a16cb --- /dev/null +++ b/sdk/helper/authmetadata/auth_metadata_acc_test.go @@ -0,0 +1,477 @@ +package authmetadata + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +type environment struct { + ctx context.Context + storage logical.Storage + backend logical.Backend +} + +func TestAcceptance(t *testing.T) { + ctx := context.Background() + storage := &logical.InmemStorage{} + b, err := backend(ctx, storage) + if err != nil { + t.Fatal(err) + } + env := &environment{ + ctx: ctx, + storage: storage, + backend: b, + } + t.Run("test initial fields are default", env.TestInitialFieldsAreDefault) + t.Run("test fields can be unset", env.TestAuthMetadataCanBeUnset) + t.Run("test defaults can be restored", env.TestDefaultCanBeReused) + t.Run("test default plus more cannot be selected", env.TestDefaultPlusMoreCannotBeSelected) + t.Run("test only non-defaults can be selected", env.TestOnlyNonDefaultsCanBeSelected) + t.Run("test bad field results in useful error", env.TestAddingBadField) +} + +func (e *environment) TestInitialFieldsAreDefault(t *testing.T) { + // On the first read of auth_metadata, when nothing has been touched, + // we should receive the default field(s) if a read is performed. + resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.ReadOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil { + t.Fatal("expected non-nil response") + } + if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"role_name"}) { + t.Fatal("expected default field of role_name to be returned") + } + + // The auth should only have the default metadata. + resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + "role_name": "something", + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil { + t.Fatalf("expected alias metadata") + } + if len(resp.Auth.Alias.Metadata) != 1 { + t.Fatal("expected only 1 field") + } + if resp.Auth.Alias.Metadata["role_name"] != "something" { + t.Fatal("expected role_name to be something") + } +} + +func (e *environment) TestAuthMetadataCanBeUnset(t *testing.T) { + // We should be able to set the auth_metadata to empty by sending an + // explicitly empty array. + resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + authMetadataFields.FieldName: []string{}, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatal("expected nil response") + } + + // Now we should receive no fields for auth_metadata. + resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.ReadOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil { + t.Fatal("expected non-nil response") + } + if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{}) { + t.Fatal("expected no fields to be returned") + } + + // The auth should have no metadata. + resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + "role_name": "something", + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil { + t.Fatal("expected alias metadata") + } + if len(resp.Auth.Alias.Metadata) != 0 { + t.Fatal("expected 0 fields") + } +} + +func (e *environment) TestDefaultCanBeReused(t *testing.T) { + // Now if we set it to "default", the default fields should + // be restored. + resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + authMetadataFields.FieldName: []string{"default"}, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatal("expected nil response") + } + + // Let's make sure we've returned to the default fields. + resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.ReadOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil { + t.Fatal("expected non-nil response") + } + if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"role_name"}) { + t.Fatal("expected default field of role_name to be returned") + } + + // We should again only receive the default field on the login. + resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + "role_name": "something", + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil { + t.Fatal("expected alias metadata") + } + if len(resp.Auth.Alias.Metadata) != 1 { + t.Fatal("expected only 1 field") + } + if resp.Auth.Alias.Metadata["role_name"] != "something" { + t.Fatal("expected role_name to be something") + } +} + +func (e *environment) TestDefaultPlusMoreCannotBeSelected(t *testing.T) { + // We should not be able to set it to "default" plus 1 optional field. + _, err := e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + authMetadataFields.FieldName: []string{"default", "remote_addr"}, + }, + }) + if err == nil { + t.Fatal("expected err") + } +} + +func (e *environment) TestOnlyNonDefaultsCanBeSelected(t *testing.T) { + // Omit all default fields and just select one. + resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + authMetadataFields.FieldName: []string{"remote_addr"}, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatal("expected nil response") + } + + // Make sure that worked. + resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.ReadOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil { + t.Fatal("expected non-nil response") + } + if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"remote_addr"}) { + t.Fatal("expected remote_addr to be returned") + } + + // Ensure only the selected one is on logins. + // They both should now appear on the login. + resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + "role_name": "something", + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil { + t.Fatal("expected alias metadata") + } + if len(resp.Auth.Alias.Metadata) != 1 { + t.Fatal("expected only 1 field") + } + if resp.Auth.Alias.Metadata["remote_addr"] != "http://foo.com" { + t.Fatal("expected remote_addr to be http://foo.com") + } +} + +func (e *environment) TestAddingBadField(t *testing.T) { + // Try adding an unsupported field. + resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: e.storage, + Connection: &logical.Connection{ + RemoteAddr: "http://foo.com", + }, + Data: map[string]interface{}{ + authMetadataFields.FieldName: []string{"asl;dfkj"}, + }, + }) + if err == nil { + t.Fatal("expected err") + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if !resp.IsError() { + t.Fatal("expected error response") + } +} + +// We expect people to embed the Handler on their +// config so it automatically makes its helper methods +// available and easy to find wherever the config is +// needed. Explicitly naming it in json avoids it +// automatically being named "Handler" by Go's JSON +// marshalling library. +type fakeConfig struct { + *Handler `json:"auth_metadata_handler"` +} + +type fakeBackend struct { + *framework.Backend +} + +// We expect each back-end to explicitly define the fields that +// will be included by default, and optionally available. +var authMetadataFields = &Fields{ + FieldName: "some_field_name", + Default: []string{ + "role_name", // This would likely never change because the alias is the role name. + }, + AvailableToAdd: []string{ + "remote_addr", // This would likely change with every new caller. + }, +} + +func configPath() *framework.Path { + return &framework.Path{ + Pattern: "config", + Fields: map[string]*framework.FieldSchema{ + authMetadataFields.FieldName: FieldSchema(authMetadataFields), + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) { + entryRaw, err := req.Storage.Get(ctx, "config") + if err != nil { + return nil, err + } + conf := &fakeConfig{ + Handler: NewHandler(authMetadataFields), + } + if entryRaw != nil { + if err := entryRaw.DecodeJSON(conf); err != nil { + return nil, err + } + } + // Note that even if the config entry was nil, we return + // a populated response to give info on what the default + // auth metadata is when unconfigured. + return &logical.Response{ + Data: map[string]interface{}{ + authMetadataFields.FieldName: conf.AuthMetadata(), + }, + }, nil + }, + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) { + entryRaw, err := req.Storage.Get(ctx, "config") + if err != nil { + return nil, err + } + conf := &fakeConfig{ + Handler: NewHandler(authMetadataFields), + } + if entryRaw != nil { + if err := entryRaw.DecodeJSON(conf); err != nil { + return nil, err + } + } + // This is where we read in the user's given auth metadata. + if err := conf.ParseAuthMetadata(fd); err != nil { + // Since this will only error on bad input, it's best to give + // a 400 response with the explicit problem included. + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + entry, err := logical.StorageEntryJSON("config", conf) + if err != nil { + return nil, err + } + if err = req.Storage.Put(ctx, entry); err != nil { + return nil, err + } + return nil, nil + }, + }, + }, + } +} + +func loginPath() *framework.Path { + return &framework.Path{ + Pattern: "login", + Fields: map[string]*framework.FieldSchema{ + "role_name": { + Type: framework.TypeString, + Required: true, + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) { + entryRaw, err := req.Storage.Get(ctx, "config") + if err != nil { + return nil, err + } + conf := &fakeConfig{ + Handler: NewHandler(authMetadataFields), + } + if entryRaw != nil { + if err := entryRaw.DecodeJSON(conf); err != nil { + return nil, err + } + } + auth := &logical.Auth{ + Alias: &logical.Alias{ + Name: fd.Get("role_name").(string), + }, + } + // Here we provide everything and let the method strip out + // the undesired stuff. + if err := conf.PopulateDesiredMetadata(auth, map[string]string{ + "role_name": fd.Get("role_name").(string), + "remote_addr": req.Connection.RemoteAddr, + }); err != nil { + fmt.Println("unable to populate due to " + err.Error()) + } + return &logical.Response{ + Auth: auth, + }, nil + }, + }, + }, + } +} + +func backend(ctx context.Context, storage logical.Storage) (logical.Backend, error) { + b := &fakeBackend{ + Backend: &framework.Backend{ + Paths: []*framework.Path{ + configPath(), + loginPath(), + }, + }, + } + if err := b.Setup(ctx, &logical.BackendConfig{ + StorageView: storage, + Logger: hclog.Default(), + }); err != nil { + return nil, err + } + return b, nil +} diff --git a/sdk/helper/authmetadata/auth_metadata_test.go b/sdk/helper/authmetadata/auth_metadata_test.go new file mode 100644 index 0000000000000..62341ebc85fbb --- /dev/null +++ b/sdk/helper/authmetadata/auth_metadata_test.go @@ -0,0 +1,127 @@ +package authmetadata + +import ( + "fmt" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +var testFields = &Fields{ + FieldName: "some-field-name", + Default: []string{"fizz", "buzz"}, + AvailableToAdd: []string{"foo", "bar"}, +} + +func TestFieldSchema(t *testing.T) { + schema := FieldSchema(testFields) + if schema.Type != framework.TypeCommaStringSlice { + t.Fatal("expected TypeCommaStringSlice") + } + if schema.Description != `The metadata to include on the aliases and audit logs generated by this plugin. When set to 'default', includes: fizz, buzz. These fields are available to add: foo, bar. Not editing this field means the 'default' fields are included. Explicitly setting this field to empty overrides the 'default' and means no metadata will be included. If not using 'default', explicit fields must be sent like: 'field1,field2'.` { + t.Fatal("received unexpected description: " + schema.Description) + } + if schema.DisplayAttrs == nil { + t.Fatal("expected display attributes") + } + if schema.DisplayAttrs.Name != testFields.FieldName { + t.Fatalf("expected name of %s", testFields.FieldName) + } + if schema.DisplayAttrs.Value != "field1,field2" { + t.Fatal("expected field1,field2") + } + if !reflect.DeepEqual(schema.Default, []string{"default"}) { + t.Fatal("expected default") + } +} + +func TestGetAuthMetadata(t *testing.T) { + h := NewHandler(testFields) + expected := []string{"fizz", "buzz"} + sort.Strings(expected) + actual := h.AuthMetadata() + sort.Strings(actual) + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected %s but received %s", expected, actual) + } +} + +func TestParseAuthMetadata(t *testing.T) { + h := NewHandler(testFields) + data := &framework.FieldData{ + Raw: map[string]interface{}{ + testFields.FieldName: []string{"default"}, + }, + Schema: map[string]*framework.FieldSchema{ + testFields.FieldName: FieldSchema(testFields), + }, + } + if err := h.ParseAuthMetadata(data); err != nil { + t.Fatal(err) + } + expected := []string{"fizz", "buzz"} + sort.Strings(expected) + actual := h.AuthMetadata() + sort.Strings(actual) + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected %s but received %s", expected, actual) + } +} + +func TestPopulateDesiredAuthMetadata(t *testing.T) { + h := NewHandler(testFields) + data := &framework.FieldData{ + Raw: map[string]interface{}{ + testFields.FieldName: []string{"foo"}, + }, + Schema: map[string]*framework.FieldSchema{ + testFields.FieldName: FieldSchema(testFields), + }, + } + if err := h.ParseAuthMetadata(data); err != nil { + t.Fatal(err) + } + auth := &logical.Auth{ + Alias: &logical.Alias{ + Name: "foo", + }, + } + if err := h.PopulateDesiredMetadata(auth, map[string]string{ + "fizz": "fizzval", + "buzz": "buzzval", + "foo": "fooval", + }); err != nil { + t.Fatal(err) + } + if len(auth.Alias.Metadata) != 1 { + t.Fatal("expected only 1 configured field to be populated") + } + if auth.Alias.Metadata["foo"] != "fooval" { + t.Fatal("expected foova;") + } +} + +func TestMarshalJSON(t *testing.T) { + h := NewHandler(&Fields{}) + h.authMetadata = []string{"fizz", "buzz"} + b, err := h.MarshalJSON() + if err != nil { + t.Fatal(err) + } + if string(b) != `{"auth_metadata":["fizz","buzz"]}` { + t.Fatal(`expected {"auth_metadata":["fizz","buzz"]}`) + } +} + +func TestUnmarshalJSON(t *testing.T) { + h := NewHandler(&Fields{}) + if err := h.UnmarshalJSON([]byte(`{"auth_metadata":["fizz","buzz"]}`)); err != nil { + t.Fatal(err) + } + if fmt.Sprintf("%s", h.authMetadata) != `[fizz buzz]` { + t.Fatal(`expected [fizz buzz]`) + } +} diff --git a/sdk/logical/identity.pb.go b/sdk/logical/identity.pb.go index 94102de78d625..26ba18a4e2b0e 100644 --- a/sdk/logical/identity.pb.go +++ b/sdk/logical/identity.pb.go @@ -114,7 +114,12 @@ type Alias struct { MountAccessor string `sentinel:"" protobuf:"bytes,2,opt,name=mount_accessor,json=mountAccessor,proto3" json:"mount_accessor,omitempty"` // Name is the identifier of this identity in its authentication source Name string `sentinel:"" protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - // Metadata represents the custom data tied to this alias + // Metadata represents the custom data tied to this alias. Fields added + // to it should have a low rate of change (or no change) because each + // change incurs a storage write, so quickly-changing fields can have + // a significant performance impact at scale. See the SDK's + // "aliasmetadata" package for a helper that eases and standardizes + // using this safely. Metadata map[string]string `sentinel:"" protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // ID is the unique identifier for the alias ID string `sentinel:"" protobuf:"bytes,5,opt,name=ID,proto3" json:"ID,omitempty"` @@ -270,31 +275,33 @@ func init() { proto.RegisterMapType((map[string]string)(nil), "logical.Group.MetadataEntry") } -func init() { proto.RegisterFile("sdk/logical/identity.proto", fileDescriptor_4a34d35719c603a1) } +func init() { + proto.RegisterFile("sdk/logical/identity.proto", fileDescriptor_4a34d35719c603a1) +} var fileDescriptor_4a34d35719c603a1 = []byte{ - // 365 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x93, 0x4f, 0x6b, 0xfa, 0x30, - 0x18, 0xc7, 0x69, 0x6b, 0xfd, 0xf3, 0xf8, 0x53, 0x7e, 0x84, 0x1d, 0x8a, 0x4c, 0x70, 0xc2, 0x46, - 0x4f, 0x2d, 0x6c, 0x17, 0xb7, 0x9d, 0x1c, 0xca, 0xf0, 0xb0, 0x4b, 0xd9, 0x69, 0x17, 0x89, 0x4d, - 0xd0, 0x60, 0xdb, 0x94, 0x26, 0x15, 0xfa, 0x1a, 0xc6, 0x5e, 0xd6, 0xde, 0xd7, 0x30, 0x8d, 0xa5, - 0x3a, 0x84, 0x1d, 0xb6, 0x5b, 0xf2, 0x7d, 0x9e, 0x7e, 0x9b, 0xcf, 0x37, 0x79, 0x60, 0x20, 0xc8, - 0xd6, 0x8f, 0xf8, 0x9a, 0x85, 0x38, 0xf2, 0x19, 0xa1, 0x89, 0x64, 0xb2, 0xf0, 0xd2, 0x8c, 0x4b, - 0x8e, 0x5a, 0x5a, 0x1f, 0x7f, 0x98, 0xd0, 0x9c, 0xab, 0x0a, 0xea, 0x83, 0xb9, 0x98, 0x39, 0xc6, - 0xc8, 0x70, 0x3b, 0x81, 0xb9, 0x98, 0x21, 0x04, 0x8d, 0x04, 0xc7, 0xd4, 0x31, 0x95, 0xa2, 0xd6, - 0xc8, 0x85, 0x16, 0x8e, 0x18, 0x16, 0x54, 0x38, 0xd6, 0xc8, 0x72, 0xbb, 0xb7, 0x7d, 0x4f, 0x3b, - 0x79, 0xd3, 0xbd, 0x1e, 0x1c, 0xca, 0xe8, 0x1e, 0xda, 0x31, 0x95, 0x98, 0x60, 0x89, 0x9d, 0x86, - 0x6a, 0x1d, 0x56, 0xad, 0xe5, 0x0f, 0xbd, 0x17, 0x5d, 0x9f, 0x27, 0x32, 0x2b, 0x82, 0xaa, 0x1d, - 0x0d, 0xa0, 0x4d, 0x98, 0xc0, 0xab, 0x88, 0x12, 0xc7, 0x1e, 0x19, 0x6e, 0x3b, 0xa8, 0xf6, 0xe8, - 0x0a, 0xfe, 0xed, 0x0f, 0x22, 0x52, 0x1c, 0xd2, 0x25, 0x23, 0x4e, 0x53, 0x1d, 0xae, 0x5b, 0x69, - 0x0b, 0x32, 0x78, 0x84, 0xde, 0x91, 0x33, 0xfa, 0x0f, 0xd6, 0x96, 0x16, 0x9a, 0x6c, 0xbf, 0x44, - 0x17, 0x60, 0xef, 0x70, 0x94, 0x1f, 0xd8, 0xca, 0xcd, 0x83, 0x39, 0x31, 0xc6, 0xef, 0x26, 0xd8, - 0x8a, 0x04, 0x0d, 0x01, 0x62, 0x9e, 0x27, 0x72, 0x29, 0x8b, 0x94, 0xea, 0x8f, 0x3b, 0x4a, 0x79, - 0x2d, 0x52, 0x8a, 0xae, 0xa1, 0x5f, 0x96, 0x71, 0x18, 0x52, 0x21, 0x78, 0xa6, 0xbd, 0x7a, 0x4a, - 0x9d, 0x6a, 0xb1, 0x0a, 0xd1, 0xaa, 0x85, 0x38, 0xf9, 0x16, 0xcd, 0xe5, 0x71, 0x8a, 0x67, 0x93, - 0x29, 0xaf, 0xc8, 0xae, 0xae, 0xe8, 0xaf, 0xd3, 0xf8, 0x34, 0xc0, 0x7e, 0xce, 0x78, 0x9e, 0xfe, - 0xe8, 0x71, 0xd4, 0xb9, 0xac, 0x13, 0x2e, 0xe5, 0x72, 0x96, 0xeb, 0x94, 0xa3, 0xf1, 0xbb, 0x1c, - 0x4f, 0xee, 0xdb, 0xcd, 0x9a, 0xc9, 0x4d, 0xbe, 0xf2, 0x42, 0x1e, 0xfb, 0x1b, 0x2c, 0x36, 0x2c, - 0xe4, 0x59, 0xea, 0xef, 0x70, 0x1e, 0x49, 0xbf, 0x36, 0x27, 0xab, 0xa6, 0x9a, 0x8f, 0xbb, 0xaf, - 0x00, 0x00, 0x00, 0xff, 0xff, 0xa8, 0xe2, 0x28, 0xc0, 0x3d, 0x03, 0x00, 0x00, + // 363 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xb4, 0x93, 0x4f, 0x4b, 0xf3, 0x40, + 0x10, 0xc6, 0x49, 0xd2, 0xf4, 0xcf, 0xf4, 0x6d, 0x79, 0x59, 0x3c, 0x84, 0x62, 0xa1, 0x16, 0x94, + 0x9c, 0x12, 0xd0, 0x4b, 0xd5, 0x53, 0x45, 0x91, 0x1e, 0xbc, 0x04, 0x4f, 0x5e, 0xca, 0x36, 0xbb, + 0xb4, 0x4b, 0x93, 0x6e, 0xc8, 0x6e, 0x0a, 0xf9, 0x0c, 0xe2, 0xc7, 0xf2, 0x7b, 0x99, 0x6e, 0xb6, + 0x21, 0xad, 0x14, 0x3c, 0xe8, 0x6d, 0xe6, 0x99, 0xc9, 0xec, 0xfe, 0x9e, 0xcd, 0xc0, 0x40, 0x90, + 0xb5, 0x1f, 0xf1, 0x25, 0x0b, 0x71, 0xe4, 0x33, 0x42, 0x37, 0x92, 0xc9, 0xdc, 0x4b, 0x52, 0x2e, + 0x39, 0x6a, 0x69, 0x7d, 0xfc, 0x61, 0x42, 0xf3, 0x49, 0x55, 0x50, 0x1f, 0xcc, 0xd9, 0xa3, 0x63, + 0x8c, 0x0c, 0xb7, 0x13, 0x14, 0x11, 0x42, 0xd0, 0xd8, 0xe0, 0x98, 0x3a, 0xa6, 0x52, 0x54, 0x8c, + 0x5c, 0x68, 0xe1, 0x88, 0x61, 0x41, 0x85, 0x63, 0x8d, 0x2c, 0xb7, 0x7b, 0xdd, 0xf7, 0xf4, 0x24, + 0x6f, 0xba, 0xd3, 0x83, 0x7d, 0x19, 0xdd, 0x42, 0x3b, 0xa6, 0x12, 0x13, 0x2c, 0xb1, 0xd3, 0x50, + 0xad, 0xc3, 0xaa, 0xb5, 0x3c, 0xd0, 0x7b, 0xd1, 0xf5, 0x22, 0x4d, 0xf3, 0xa0, 0x6a, 0x47, 0x03, + 0x68, 0x13, 0x26, 0xf0, 0x22, 0xa2, 0xc4, 0xb1, 0x8b, 0xc3, 0xdb, 0x41, 0x95, 0xa3, 0x0b, 0xf8, + 0xb7, 0xbb, 0x88, 0x48, 0x70, 0x48, 0xe7, 0x8c, 0x38, 0x4d, 0x75, 0xb9, 0x6e, 0xa5, 0xcd, 0xc8, + 0xe0, 0x1e, 0x7a, 0x07, 0x93, 0xd1, 0x7f, 0xb0, 0xd6, 0x34, 0xd7, 0x64, 0xbb, 0x10, 0x9d, 0x81, + 0xbd, 0xc5, 0x51, 0xb6, 0x67, 0x2b, 0x93, 0x3b, 0x73, 0x62, 0x8c, 0xdf, 0x4d, 0xb0, 0x15, 0x09, + 0x1a, 0x02, 0xc4, 0x3c, 0xdb, 0xc8, 0xb9, 0xcc, 0x13, 0xaa, 0x3f, 0xee, 0x28, 0xe5, 0xb5, 0x10, + 0xd0, 0x25, 0xf4, 0xcb, 0x32, 0x0e, 0x43, 0x2a, 0x04, 0x4f, 0xf5, 0xac, 0x9e, 0x52, 0xa7, 0x5a, + 0xac, 0x4c, 0xb4, 0x6a, 0x26, 0x4e, 0xbe, 0x59, 0x73, 0x7e, 0xe8, 0xe2, 0x49, 0x67, 0xca, 0x27, + 0xb2, 0xab, 0x27, 0xfa, 0x6b, 0x37, 0x3e, 0x0d, 0xb0, 0x9f, 0x53, 0x9e, 0x25, 0x3f, 0xfa, 0x39, + 0xea, 0x5c, 0xd6, 0x11, 0x97, 0x9a, 0x72, 0x92, 0xeb, 0x98, 0xa3, 0xf1, 0xbb, 0x1c, 0x0f, 0xee, + 0xdb, 0xd5, 0x92, 0xc9, 0x55, 0xb6, 0xf0, 0x42, 0x1e, 0xfb, 0x2b, 0x2c, 0x56, 0x2c, 0xe4, 0x69, + 0xe2, 0x6f, 0x71, 0x16, 0x49, 0xbf, 0xb6, 0x27, 0x8b, 0xa6, 0xda, 0x8f, 0x9b, 0xaf, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xa8, 0xe2, 0x28, 0xc0, 0x3d, 0x03, 0x00, 0x00, } diff --git a/sdk/logical/identity.proto b/sdk/logical/identity.proto index 34af579b4d7a9..78c3758f8510f 100644 --- a/sdk/logical/identity.proto +++ b/sdk/logical/identity.proto @@ -36,7 +36,12 @@ message Alias { // Name is the identifier of this identity in its authentication source string name = 3; - // Metadata represents the custom data tied to this alias + // Metadata represents the custom data tied to this alias. Fields added + // to it should have a low rate of change (or no change) because each + // change incurs a storage write, so quickly-changing fields can have + // a significant performance impact at scale. See the SDK's + // "aliasmetadata" package for a helper that eases and standardizes + // using this safely. map metadata = 4; // ID is the unique identifier for the alias diff --git a/vendor/github.com/hashicorp/vault/sdk/helper/authmetadata/auth_metadata.go b/vendor/github.com/hashicorp/vault/sdk/helper/authmetadata/auth_metadata.go new file mode 100644 index 0000000000000..c1e4e93d59369 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/sdk/helper/authmetadata/auth_metadata.go @@ -0,0 +1,200 @@ +package authmetadata + +/* + authmetadata is a package offering convenience and + standardization when supporting an `auth_metadata` + field in a plugin's configuration. This then controls + what 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 ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/strutil" + "github.com/hashicorp/vault/sdk/logical" +) + +// 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 { + // The field name as it'll be reflected in the user-facing + // schema. + FieldName string + + // 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 "auth_metadata" field. +func FieldSchema(fields *Fields) *framework.FieldSchema { + return &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: description(fields), + DisplayAttrs: &framework.DisplayAttributes{ + Name: fields.FieldName, + Value: "field1,field2", + }, + Default: []string{"default"}, + } +} + +func NewHandler(fields *Fields) *Handler { + return &Handler{ + fields: fields, + } +} + +type Handler struct { + // authMetadata is an explicit list of all the user's configured + // fields that are being added to auth metadata. If it is set to + // default or unconfigured, it will be nil. Otherwise, it will + // hold the explicit fields set by the user. + authMetadata []string + + // fields is a list of the configured default and available + // fields. + fields *Fields +} + +// AuthMetadata 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 auth metadata. +func (h *Handler) AuthMetadata() []string { + if h.authMetadata == nil { + return h.fields.Default + } + return h.authMetadata +} + +// ParseAuthMetadata 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) ParseAuthMetadata(data *framework.FieldData) error { + userProvidedRaw, ok := data.GetOk(h.fields.FieldName) + if !ok { + // Nothing further to do here. + return nil + } + userProvided, ok := userProvidedRaw.([]string) + if !ok { + return fmt.Errorf("%s is an unexpected type of %T", userProvidedRaw, userProvidedRaw) + } + userProvided = strutil.RemoveDuplicates(userProvided, true) + + // If the only field the user has chosen was the default field, + // we don't store anything so we won't have to do a storage + // migration if the default changes. + if len(userProvided) == 1 && userProvided[0] == "default" { + h.authMetadata = nil + return nil + } + + // Validate and store the input. + if strutil.StrListContains(userProvided, "default") { + return fmt.Errorf("%q contains default - default can't be used in combination with other fields", + userProvided) + } + if !strutil.StrListSubset(h.fields.all(), userProvided) { + return fmt.Errorf("%q contains an unavailable field, please select from %q", + strings.Join(userProvided, ", "), strings.Join(h.fields.all(), ", ")) + } + h.authMetadata = userProvided + return nil +} + +// PopulateDesiredMetadata is intended to be used during login +// just before returning an auth. +// It takes the available auth metadata and, +// if the auth should have it, adds it to the auth's metadata. +func (h *Handler) PopulateDesiredMetadata(auth *logical.Auth, available map[string]string) error { + if auth == nil { + return errors.New("auth is nil") + } + if auth.Metadata == nil { + auth.Metadata = make(map[string]string) + } + if auth.Alias == nil { + auth.Alias = &logical.Alias{} + } + if auth.Alias.Metadata == nil { + auth.Alias.Metadata = make(map[string]string) + } + fieldsToInclude := h.fields.Default + if h.authMetadata != nil { + fieldsToInclude = h.authMetadata + } + for availableField, itsValue := range available { + if itsValue == "" { + // Don't bother setting fields for which there is no value. + continue + } + if strutil.StrListContains(fieldsToInclude, availableField) { + auth.Metadata[availableField] = itsValue + auth.Alias.Metadata[availableField] = itsValue + } + } + return nil +} + +func (h *Handler) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + AuthMetadata []string `json:"auth_metadata"` + }{ + AuthMetadata: h.authMetadata, + }) +} + +func (h *Handler) UnmarshalJSON(data []byte) error { + jsonable := &struct { + AuthMetadata []string `json:"auth_metadata"` + }{ + AuthMetadata: h.authMetadata, + } + if err := json.Unmarshal(data, jsonable); err != nil { + return err + } + h.authMetadata = jsonable.AuthMetadata + return nil +} + +func description(fields *Fields) string { + desc := "The metadata to include on the aliases and audit logs 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 metadata will be included." + + " If not using 'default', explicit fields must be sent like: 'field1,field2'." + return desc +} diff --git a/vendor/github.com/hashicorp/vault/sdk/logical/identity.pb.go b/vendor/github.com/hashicorp/vault/sdk/logical/identity.pb.go index 94102de78d625..26ba18a4e2b0e 100644 --- a/vendor/github.com/hashicorp/vault/sdk/logical/identity.pb.go +++ b/vendor/github.com/hashicorp/vault/sdk/logical/identity.pb.go @@ -114,7 +114,12 @@ type Alias struct { MountAccessor string `sentinel:"" protobuf:"bytes,2,opt,name=mount_accessor,json=mountAccessor,proto3" json:"mount_accessor,omitempty"` // Name is the identifier of this identity in its authentication source Name string `sentinel:"" protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - // Metadata represents the custom data tied to this alias + // Metadata represents the custom data tied to this alias. Fields added + // to it should have a low rate of change (or no change) because each + // change incurs a storage write, so quickly-changing fields can have + // a significant performance impact at scale. See the SDK's + // "aliasmetadata" package for a helper that eases and standardizes + // using this safely. Metadata map[string]string `sentinel:"" protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // ID is the unique identifier for the alias ID string `sentinel:"" protobuf:"bytes,5,opt,name=ID,proto3" json:"ID,omitempty"` @@ -270,31 +275,33 @@ func init() { proto.RegisterMapType((map[string]string)(nil), "logical.Group.MetadataEntry") } -func init() { proto.RegisterFile("sdk/logical/identity.proto", fileDescriptor_4a34d35719c603a1) } +func init() { + proto.RegisterFile("sdk/logical/identity.proto", fileDescriptor_4a34d35719c603a1) +} var fileDescriptor_4a34d35719c603a1 = []byte{ - // 365 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x93, 0x4f, 0x6b, 0xfa, 0x30, - 0x18, 0xc7, 0x69, 0x6b, 0xfd, 0xf3, 0xf8, 0x53, 0x7e, 0x84, 0x1d, 0x8a, 0x4c, 0x70, 0xc2, 0x46, - 0x4f, 0x2d, 0x6c, 0x17, 0xb7, 0x9d, 0x1c, 0xca, 0xf0, 0xb0, 0x4b, 0xd9, 0x69, 0x17, 0x89, 0x4d, - 0xd0, 0x60, 0xdb, 0x94, 0x26, 0x15, 0xfa, 0x1a, 0xc6, 0x5e, 0xd6, 0xde, 0xd7, 0x30, 0x8d, 0xa5, - 0x3a, 0x84, 0x1d, 0xb6, 0x5b, 0xf2, 0x7d, 0x9e, 0x7e, 0x9b, 0xcf, 0x37, 0x79, 0x60, 0x20, 0xc8, - 0xd6, 0x8f, 0xf8, 0x9a, 0x85, 0x38, 0xf2, 0x19, 0xa1, 0x89, 0x64, 0xb2, 0xf0, 0xd2, 0x8c, 0x4b, - 0x8e, 0x5a, 0x5a, 0x1f, 0x7f, 0x98, 0xd0, 0x9c, 0xab, 0x0a, 0xea, 0x83, 0xb9, 0x98, 0x39, 0xc6, - 0xc8, 0x70, 0x3b, 0x81, 0xb9, 0x98, 0x21, 0x04, 0x8d, 0x04, 0xc7, 0xd4, 0x31, 0x95, 0xa2, 0xd6, - 0xc8, 0x85, 0x16, 0x8e, 0x18, 0x16, 0x54, 0x38, 0xd6, 0xc8, 0x72, 0xbb, 0xb7, 0x7d, 0x4f, 0x3b, - 0x79, 0xd3, 0xbd, 0x1e, 0x1c, 0xca, 0xe8, 0x1e, 0xda, 0x31, 0x95, 0x98, 0x60, 0x89, 0x9d, 0x86, - 0x6a, 0x1d, 0x56, 0xad, 0xe5, 0x0f, 0xbd, 0x17, 0x5d, 0x9f, 0x27, 0x32, 0x2b, 0x82, 0xaa, 0x1d, - 0x0d, 0xa0, 0x4d, 0x98, 0xc0, 0xab, 0x88, 0x12, 0xc7, 0x1e, 0x19, 0x6e, 0x3b, 0xa8, 0xf6, 0xe8, - 0x0a, 0xfe, 0xed, 0x0f, 0x22, 0x52, 0x1c, 0xd2, 0x25, 0x23, 0x4e, 0x53, 0x1d, 0xae, 0x5b, 0x69, - 0x0b, 0x32, 0x78, 0x84, 0xde, 0x91, 0x33, 0xfa, 0x0f, 0xd6, 0x96, 0x16, 0x9a, 0x6c, 0xbf, 0x44, - 0x17, 0x60, 0xef, 0x70, 0x94, 0x1f, 0xd8, 0xca, 0xcd, 0x83, 0x39, 0x31, 0xc6, 0xef, 0x26, 0xd8, - 0x8a, 0x04, 0x0d, 0x01, 0x62, 0x9e, 0x27, 0x72, 0x29, 0x8b, 0x94, 0xea, 0x8f, 0x3b, 0x4a, 0x79, - 0x2d, 0x52, 0x8a, 0xae, 0xa1, 0x5f, 0x96, 0x71, 0x18, 0x52, 0x21, 0x78, 0xa6, 0xbd, 0x7a, 0x4a, - 0x9d, 0x6a, 0xb1, 0x0a, 0xd1, 0xaa, 0x85, 0x38, 0xf9, 0x16, 0xcd, 0xe5, 0x71, 0x8a, 0x67, 0x93, - 0x29, 0xaf, 0xc8, 0xae, 0xae, 0xe8, 0xaf, 0xd3, 0xf8, 0x34, 0xc0, 0x7e, 0xce, 0x78, 0x9e, 0xfe, - 0xe8, 0x71, 0xd4, 0xb9, 0xac, 0x13, 0x2e, 0xe5, 0x72, 0x96, 0xeb, 0x94, 0xa3, 0xf1, 0xbb, 0x1c, - 0x4f, 0xee, 0xdb, 0xcd, 0x9a, 0xc9, 0x4d, 0xbe, 0xf2, 0x42, 0x1e, 0xfb, 0x1b, 0x2c, 0x36, 0x2c, - 0xe4, 0x59, 0xea, 0xef, 0x70, 0x1e, 0x49, 0xbf, 0x36, 0x27, 0xab, 0xa6, 0x9a, 0x8f, 0xbb, 0xaf, - 0x00, 0x00, 0x00, 0xff, 0xff, 0xa8, 0xe2, 0x28, 0xc0, 0x3d, 0x03, 0x00, 0x00, + // 363 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xb4, 0x93, 0x4f, 0x4b, 0xf3, 0x40, + 0x10, 0xc6, 0x49, 0xd2, 0xf4, 0xcf, 0xf4, 0x6d, 0x79, 0x59, 0x3c, 0x84, 0x62, 0xa1, 0x16, 0x94, + 0x9c, 0x12, 0xd0, 0x4b, 0xd5, 0x53, 0x45, 0x91, 0x1e, 0xbc, 0x04, 0x4f, 0x5e, 0xca, 0x36, 0xbb, + 0xb4, 0x4b, 0x93, 0x6e, 0xc8, 0x6e, 0x0a, 0xf9, 0x0c, 0xe2, 0xc7, 0xf2, 0x7b, 0x99, 0x6e, 0xb6, + 0x21, 0xad, 0x14, 0x3c, 0xe8, 0x6d, 0xe6, 0x99, 0xc9, 0xec, 0xfe, 0x9e, 0xcd, 0xc0, 0x40, 0x90, + 0xb5, 0x1f, 0xf1, 0x25, 0x0b, 0x71, 0xe4, 0x33, 0x42, 0x37, 0x92, 0xc9, 0xdc, 0x4b, 0x52, 0x2e, + 0x39, 0x6a, 0x69, 0x7d, 0xfc, 0x61, 0x42, 0xf3, 0x49, 0x55, 0x50, 0x1f, 0xcc, 0xd9, 0xa3, 0x63, + 0x8c, 0x0c, 0xb7, 0x13, 0x14, 0x11, 0x42, 0xd0, 0xd8, 0xe0, 0x98, 0x3a, 0xa6, 0x52, 0x54, 0x8c, + 0x5c, 0x68, 0xe1, 0x88, 0x61, 0x41, 0x85, 0x63, 0x8d, 0x2c, 0xb7, 0x7b, 0xdd, 0xf7, 0xf4, 0x24, + 0x6f, 0xba, 0xd3, 0x83, 0x7d, 0x19, 0xdd, 0x42, 0x3b, 0xa6, 0x12, 0x13, 0x2c, 0xb1, 0xd3, 0x50, + 0xad, 0xc3, 0xaa, 0xb5, 0x3c, 0xd0, 0x7b, 0xd1, 0xf5, 0x22, 0x4d, 0xf3, 0xa0, 0x6a, 0x47, 0x03, + 0x68, 0x13, 0x26, 0xf0, 0x22, 0xa2, 0xc4, 0xb1, 0x8b, 0xc3, 0xdb, 0x41, 0x95, 0xa3, 0x0b, 0xf8, + 0xb7, 0xbb, 0x88, 0x48, 0x70, 0x48, 0xe7, 0x8c, 0x38, 0x4d, 0x75, 0xb9, 0x6e, 0xa5, 0xcd, 0xc8, + 0xe0, 0x1e, 0x7a, 0x07, 0x93, 0xd1, 0x7f, 0xb0, 0xd6, 0x34, 0xd7, 0x64, 0xbb, 0x10, 0x9d, 0x81, + 0xbd, 0xc5, 0x51, 0xb6, 0x67, 0x2b, 0x93, 0x3b, 0x73, 0x62, 0x8c, 0xdf, 0x4d, 0xb0, 0x15, 0x09, + 0x1a, 0x02, 0xc4, 0x3c, 0xdb, 0xc8, 0xb9, 0xcc, 0x13, 0xaa, 0x3f, 0xee, 0x28, 0xe5, 0xb5, 0x10, + 0xd0, 0x25, 0xf4, 0xcb, 0x32, 0x0e, 0x43, 0x2a, 0x04, 0x4f, 0xf5, 0xac, 0x9e, 0x52, 0xa7, 0x5a, + 0xac, 0x4c, 0xb4, 0x6a, 0x26, 0x4e, 0xbe, 0x59, 0x73, 0x7e, 0xe8, 0xe2, 0x49, 0x67, 0xca, 0x27, + 0xb2, 0xab, 0x27, 0xfa, 0x6b, 0x37, 0x3e, 0x0d, 0xb0, 0x9f, 0x53, 0x9e, 0x25, 0x3f, 0xfa, 0x39, + 0xea, 0x5c, 0xd6, 0x11, 0x97, 0x9a, 0x72, 0x92, 0xeb, 0x98, 0xa3, 0xf1, 0xbb, 0x1c, 0x0f, 0xee, + 0xdb, 0xd5, 0x92, 0xc9, 0x55, 0xb6, 0xf0, 0x42, 0x1e, 0xfb, 0x2b, 0x2c, 0x56, 0x2c, 0xe4, 0x69, + 0xe2, 0x6f, 0x71, 0x16, 0x49, 0xbf, 0xb6, 0x27, 0x8b, 0xa6, 0xda, 0x8f, 0x9b, 0xaf, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xa8, 0xe2, 0x28, 0xc0, 0x3d, 0x03, 0x00, 0x00, } diff --git a/vendor/github.com/hashicorp/vault/sdk/logical/identity.proto b/vendor/github.com/hashicorp/vault/sdk/logical/identity.proto index 34af579b4d7a9..78c3758f8510f 100644 --- a/vendor/github.com/hashicorp/vault/sdk/logical/identity.proto +++ b/vendor/github.com/hashicorp/vault/sdk/logical/identity.proto @@ -36,7 +36,12 @@ message Alias { // Name is the identifier of this identity in its authentication source string name = 3; - // Metadata represents the custom data tied to this alias + // Metadata represents the custom data tied to this alias. Fields added + // to it should have a low rate of change (or no change) because each + // change incurs a storage write, so quickly-changing fields can have + // a significant performance impact at scale. See the SDK's + // "aliasmetadata" package for a helper that eases and standardizes + // using this safely. map metadata = 4; // ID is the unique identifier for the alias