From e75f1d2f73e4d672e404a5159080d78c27fc48c5 Mon Sep 17 00:00:00 2001 From: Louis Ruch Date: Wed, 8 Dec 2021 15:35:41 -0800 Subject: [PATCH] feat(credential): Add mapping override update for API/SDK/CLI This change adds support for updating a credential mapping override on a credential library. --- .../credentiallibrariescmd/vault_funcs.go | 9 +- internal/credential/vault/fields.go | 4 +- .../vault/repository_credential_library.go | 8 +- .../credential/vault/setup_manual_test.go | 2 + .../credentiallibrary_service.go | 113 ++++++- .../credentiallibrary_service_test.go | 303 +++++++++++++++++- 6 files changed, 405 insertions(+), 34 deletions(-) diff --git a/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go index ae783aa660..6648c3cc01 100644 --- a/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go @@ -37,8 +37,13 @@ func extraVaultActionsFlagsMapFuncImpl() map[string][]string { credentialTypeFlagName, credentialMappingFlagName, }, + "update": { + pathFlagName, + httpMethodFlagName, + httpRequestBodyFlagName, + credentialMappingFlagName, + }, } - flags["update"] = flags["create"] return flags } @@ -122,7 +127,7 @@ func extraVaultFlagHandlingFuncImpl(c *VaultCommand, _ *base.FlagSets, opts *[]c mappings := make(map[string]interface{}, len(c.flagCredentialMapping)) for _, mapping := range c.flagCredentialMapping { switch { - case len(mapping.Keys) != 1 || mapping.Keys[0] == "": + case len(mapping.Keys) != 1 || mapping.Keys[0] == "" || mapping.Value == "": // mapping override does not support key segments (e.g. 'x.y=z') c.UI.Error("Credential mapping override must be in the format 'key=value', 'key=null' to clear field or 'null' to clear all.") return false diff --git a/internal/credential/vault/fields.go b/internal/credential/vault/fields.go index cad36b46bf..42826fdb6a 100644 --- a/internal/credential/vault/fields.go +++ b/internal/credential/vault/fields.go @@ -18,5 +18,7 @@ const ( tlsSkipVerifyField = "TlsSkipVerify" tokenField = "Token" - mappingOverrideField = "MappingOverride" + // MappingOverrideField represents the field mask indicating a mapping override + // update has been requested. + MappingOverrideField = "MappingOverride" ) diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index 1ded5ea07e..3e3ffb2dfe 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -149,7 +149,7 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string case strings.EqualFold(vaultPathField, f): case strings.EqualFold(httpMethodField, f): case strings.EqualFold(httpRequestBodyField, f): - case strings.EqualFold(mappingOverrideField, f): + case strings.EqualFold(MappingOverrideField, f): updateMappingOverride = true default: return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) @@ -163,7 +163,7 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string vaultPathField: l.VaultPath, httpMethodField: l.HttpMethod, httpRequestBodyField: l.HttpRequestBody, - mappingOverrideField: l.MappingOverride, + MappingOverrideField: l.MappingOverride, }, fieldMaskPaths, nil, @@ -198,14 +198,14 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string var filteredDbMask, filteredNullFields []string for _, f := range dbMask { switch { - case strings.EqualFold(mappingOverrideField, f): + case strings.EqualFold(MappingOverrideField, f): default: filteredDbMask = append(filteredDbMask, f) } } for _, f := range nullFields { switch { - case strings.EqualFold(mappingOverrideField, f): + case strings.EqualFold(MappingOverrideField, f): default: filteredNullFields = append(filteredNullFields, f) } diff --git a/internal/credential/vault/setup_manual_test.go b/internal/credential/vault/setup_manual_test.go index 971dcfc38a..dab4c0a38a 100644 --- a/internal/credential/vault/setup_manual_test.go +++ b/internal/credential/vault/setup_manual_test.go @@ -20,6 +20,8 @@ import ( "github.com/hashicorp/boundary/internal/servers/worker" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" + + _ "github.com/hashicorp/boundary/internal/servers/controller/handlers/targets/tcp" ) func TestSetupSleepyDevEnvironment(t *testing.T) { diff --git a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go index 1667e4d623..349b612950 100644 --- a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -21,14 +21,16 @@ import ( pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentiallibraries" "github.com/hashicorp/go-secure-stdlib/strutil" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" ) const ( - vaultPathField = "attributes.path" - httpMethodField = "attributes.http_method" - httpRequestBodyField = "attributes.http_request_body" + vaultPathField = "attributes.path" + httpMethodField = "attributes.http_method" + httpRequestBodyField = "attributes.http_request_body" + credentialMappingPathField = "credential_mapping_overrides" ) // Credential mapping override attributes @@ -227,14 +229,24 @@ func (s Service) CreateCredentialLibrary(ctx context.Context, req *pbs.CreateCre func (s Service) UpdateCredentialLibrary(ctx context.Context, req *pbs.UpdateCredentialLibraryRequest) (*pbs.UpdateCredentialLibraryResponse, error) { const op = "credentiallibraries.(Service).UpdateCredentialLibrary" - if err := validateUpdateRequest(req); err != nil { + repo, err := s.repoFn() + if err != nil { + return nil, err + } + cur, err := repo.LookupCredentialLibrary(ctx, req.Id) + if err != nil { + return nil, err + } + currentCredentialType := credential.Type(cur.GetCredentialType()) + + if err := validateUpdateRequest(req, currentCredentialType); err != nil { return nil, err } authResults := s.authResult(ctx, req.GetId(), action.Update) if authResults.Error != nil { return nil, authResults.Error } - cl, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + cl, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem(), currentCredentialType, cur.MappingOverride) if err != nil { return nil, err } @@ -326,23 +338,46 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Cred return out, nil } -func (s Service) updateInRepo(ctx context.Context, projId, id string, mask []string, item *pb.CredentialLibrary) (credential.Library, error) { +func (s Service) updateInRepo( + ctx context.Context, + projId, id string, + masks []string, + in *pb.CredentialLibrary, + currentCredentialType credential.Type, + currentMapping vault.MappingOverride) (credential.Library, error) { + const op = "credentiallibraries.(Service).updateInRepo" + + var dbMasks []string + item := proto.Clone(in).(*pb.CredentialLibrary) + item.CredentialType = string(currentCredentialType) + + mapping, update := getMappingUpdates(currentCredentialType, currentMapping, item.GetCredentialMappingOverrides().AsMap(), masks) + if update { + // got mapping update, append mapping override db field mask + dbMasks = append(dbMasks, vault.MappingOverrideField) + mappingStruct, err := structpb.NewStruct(mapping) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + item.CredentialMappingOverrides = mappingStruct + } + cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) if err != nil { return nil, errors.Wrap(ctx, err, op) } cl.PublicId = id - dbMask := maskManager.Translate(mask) - if len(dbMask) == 0 { + dbMasks = append(dbMasks, maskManager.Translate(masks)...) + if len(dbMasks) == 0 { return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) } repo, err := s.repoFn() if err != nil { return nil, errors.Wrap(ctx, err, op) } - out, rowsUpdated, err := repo.UpdateCredentialLibrary(ctx, projId, cl, item.GetVersion(), dbMask) + out, rowsUpdated, err := repo.UpdateCredentialLibrary(ctx, projId, cl, item.GetVersion(), dbMasks) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) } @@ -596,7 +631,7 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { if b := attrs.GetHttpRequestBody(); b != nil && strings.ToUpper(attrs.GetHttpMethod().GetValue()) != "POST" { badFields[httpRequestBodyField] = fmt.Sprintf("Field can only be set if %q is set to the value 'POST'.", httpMethodField) } - validateMapping(badFields, req.GetItem().GetCredentialType(), req.GetItem().CredentialMappingOverrides.AsMap()) + validateMapping(badFields, credential.Type(req.GetItem().GetCredentialType()), req.GetItem().CredentialMappingOverrides.AsMap()) default: badFields[globals.CredentialStoreIdField] = "This field must be a valid credential store id." } @@ -604,7 +639,7 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { }) } -func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest) error { +func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest, currentCredentialType credential.Type) error { return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} switch credential.SubtypeFromId(req.GetId()) { @@ -612,6 +647,9 @@ func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest) error { if req.GetItem().GetType() != "" && credential.SubtypeFromType(req.GetItem().GetType()) != vault.Subtype { badFields[globals.TypeField] = "Cannot modify resource type." } + if req.GetItem().GetCredentialType() != "" && req.GetItem().GetCredentialType() != string(currentCredentialType) { + badFields[globals.CredentialTypeField] = "Cannot modify credential type." + } attrs := &pb.VaultCredentialLibraryAttributes{} if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { badFields[globals.AttributesField] = "Attribute fields do not match the expected format." @@ -626,6 +664,7 @@ func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest) error { if b := attrs.GetHttpRequestBody(); b != nil && strings.ToUpper(attrs.GetHttpMethod().GetValue()) == "GET" { badFields[httpRequestBodyField] = fmt.Sprintf("Field can only be set if %q is set to the value 'POST'.", httpMethodField) } + validateMapping(badFields, currentCredentialType, req.GetItem().CredentialMappingOverrides.AsMap()) } return badFields }, vault.CredentialLibraryPrefix) @@ -649,10 +688,10 @@ func validateListRequest(req *pbs.ListCredentialLibrariesRequest) error { return nil } -func validateMapping(badFields map[string]string, credentialType string, overrides map[string]interface{}) { +func validateMapping(badFields map[string]string, credentialType credential.Type, overrides map[string]interface{}) { validFields := make(map[string]bool) - switch credential.Type(credentialType) { - case "": + switch credentialType { + case "", credential.UnspecifiedType: if len(overrides) > 0 { badFields[globals.CredentialMappingOverridesField] = fmt.Sprintf("This field can only be set if %q is set", globals.CredentialTypeField) } @@ -675,3 +714,49 @@ func validateMapping(badFields map[string]string, credentialType string, overrid } } } + +func getMappingUpdates(credentialType credential.Type, current vault.MappingOverride, new map[string]interface{}, apiMasks []string) (map[string]interface{}, bool) { + ret := make(map[string]interface{}) + masks := make(map[string]bool) + for _, m := range apiMasks { + if m == credentialMappingPathField { + // got top level credential mapping change request, this mask + // can only be provided when clearing the entire override. + return nil, true + } + + credMappingPrefix := fmt.Sprintf("%v.", credentialMappingPathField) + if s := strings.SplitN(m, credMappingPrefix, 2); len(s) == 2 { + masks[s[1]] = true + } + } + if len(masks) == 0 { + // no mapping updates + return nil, false + } + + switch credentialType { + case credential.UserPasswordType: + var currentUser, currentPass interface{} + if overrides, ok := current.(*vault.UserPasswordOverride); ok { + currentUser = overrides.UsernameAttribute + currentPass = overrides.PasswordAttribute + } + + switch { + case masks[usernameAttribute]: + ret[usernameAttribute] = new[usernameAttribute] + default: + ret[usernameAttribute] = currentUser + } + + switch { + case masks[passwordAttribute]: + ret[passwordAttribute] = new[passwordAttribute] + default: + ret[passwordAttribute] = currentPass + } + } + + return ret, true +} diff --git a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index 3e1e7e5b98..38a890296c 100644 --- a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -1,6 +1,7 @@ package credentiallibraries import ( + "context" "fmt" "sort" "strings" @@ -648,10 +649,23 @@ func TestGet(t *testing.T) { _, prj := iam.TestScopes(t, iamRepo) store := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] - vl := vault.TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] + unspecifiedLib := vault.TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] s, err := NewService(repoFn, iamRepoFn) require.NoError(t, err) + repo, err := repoFn() + require.NoError(t, err) + lib, err := vault.NewCredentialLibrary(store.GetPublicId(), "vault/path", + vault.WithCredentialType("user_password"), + vault.WithMappingOverride( + vault.NewUserPasswordOverride( + vault.WithOverrideUsernameAttribute("user"), + vault.WithOverridePasswordAttribute("pass"), + ))) + require.NoError(t, err) + userPassLib, err := repo.CreateCredentialLibrary(context.Background(), prj.GetPublicId(), lib) + require.NoError(t, err) + cases := []struct { name string id string @@ -660,25 +674,59 @@ func TestGet(t *testing.T) { }{ { name: "success", - id: vl.GetPublicId(), + id: unspecifiedLib.GetPublicId(), + res: &pbs.GetCredentialLibraryResponse{ + Item: &pb.CredentialLibrary{ + Id: unspecifiedLib.GetPublicId(), + CredentialStoreId: unspecifiedLib.GetStoreId(), + Scope: &scopepb.ScopeInfo{Id: store.GetScopeId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + Type: vault.Subtype.String(), + AuthorizedActions: testAuthorizedActions, + CreatedTime: unspecifiedLib.CreateTime.GetTimestamp(), + UpdatedTime: unspecifiedLib.UpdateTime.GetTimestamp(), + Version: 1, + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(unspecifiedLib.GetVaultPath()), + HttpMethod: wrapperspb.String(unspecifiedLib.GetHttpMethod()), + }) + require.NoError(t, err) + return attrs + }(), + }, + }, + }, + { + name: "success-userpassword", + id: userPassLib.GetPublicId(), res: &pbs.GetCredentialLibraryResponse{ Item: &pb.CredentialLibrary{ - Id: vl.GetPublicId(), - CredentialStoreId: vl.GetStoreId(), + Id: userPassLib.GetPublicId(), + CredentialStoreId: userPassLib.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetScopeId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, Type: vault.Subtype.String(), AuthorizedActions: testAuthorizedActions, - CreatedTime: vl.CreateTime.GetTimestamp(), - UpdatedTime: vl.UpdateTime.GetTimestamp(), + CreatedTime: userPassLib.CreateTime.GetTimestamp(), + UpdatedTime: userPassLib.UpdateTime.GetTimestamp(), Version: 1, Attributes: func() *structpb.Struct { attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ - Path: wrapperspb.String(vl.GetVaultPath()), - HttpMethod: wrapperspb.String(vl.GetHttpMethod()), + Path: wrapperspb.String(userPassLib.GetVaultPath()), + HttpMethod: wrapperspb.String(userPassLib.GetHttpMethod()), }) require.NoError(t, err) return attrs }(), + CredentialType: "user_password", + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "user", + passwordAttribute: "pass", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), }, }, }, @@ -799,8 +847,14 @@ func TestUpdate(t *testing.T) { cs := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) store, diffStore := cs[0], cs[1] - freshLibrary := func() (*vault.CredentialLibrary, func()) { - vl := vault.TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] + freshLibrary := func(opt ...vault.Option) (*vault.CredentialLibrary, func()) { + repo, err := repoFn() + require.NoError(t, err) + lib, err := vault.NewCredentialLibrary(store.GetPublicId(), "vault/path", opt...) + require.NoError(t, err) + + vl, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), lib) + require.NoError(t, err) clean := func() { _, err := s.DeleteCredentialLibrary(ctx, &pbs.DeleteCredentialLibraryRequest{Id: vl.GetPublicId()}) require.NoError(t, err) @@ -816,8 +870,12 @@ func TestUpdate(t *testing.T) { _, token := v.CreateToken(t) _ = token + usernameAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, usernameAttribute) + passwordAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, passwordAttribute) + successCases := []struct { name string + opts []vault.Option req *pbs.UpdateCredentialLibraryRequest res func(*pb.CredentialLibrary) *pb.CredentialLibrary }{ @@ -853,7 +911,7 @@ func TestUpdate(t *testing.T) { }, res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { out := proto.Clone(in).(*pb.CredentialLibrary) - out.Attributes.Fields["path"] = structpb.NewStringValue("vault/path0") + out.Attributes.Fields["path"] = structpb.NewStringValue("vault/path") out.Attributes.Fields["http_method"] = structpb.NewStringValue("POST") return out }, @@ -875,7 +933,7 @@ func TestUpdate(t *testing.T) { }, res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { out := proto.Clone(in).(*pb.CredentialLibrary) - out.Attributes.Fields["path"] = structpb.NewStringValue("vault/path0") + out.Attributes.Fields["path"] = structpb.NewStringValue("vault/path") out.Attributes.Fields["http_method"] = structpb.NewStringValue("POST") out.Attributes.Fields["http_request_body"] = structpb.NewStringValue("body") return out @@ -901,12 +959,226 @@ func TestUpdate(t *testing.T) { return out }, }, + { + name: "user-password-attributes-change-username-attribute", + opts: []vault.Option{ + vault.WithCredentialType("user_password"), + vault.WithMappingOverride( + vault.NewUserPasswordOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePasswordAttribute("orig-pass"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "changed-user", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides.Fields[usernameAttribute] = structpb.NewStringValue("changed-user") + return out + }, + }, + { + name: "user-password-attributes-change-password-attribute", + opts: []vault.Option{ + vault.WithCredentialType("user_password"), + vault.WithMappingOverride( + vault.NewUserPasswordOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePasswordAttribute("orig-pass"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + passwordAttribute: "changed-pass", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides.Fields[passwordAttribute] = structpb.NewStringValue("changed-pass") + return out + }, + }, + { + name: "user-password-attributes-change-username-and-password-attributes", + opts: []vault.Option{ + vault.WithCredentialType("user_password"), + vault.WithMappingOverride( + vault.NewUserPasswordOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePasswordAttribute("orig-pass"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField, usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "changed-user", + passwordAttribute: "changed-pass", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides.Fields[usernameAttribute] = structpb.NewStringValue("changed-user") + out.CredentialMappingOverrides.Fields[passwordAttribute] = structpb.NewStringValue("changed-pass") + return out + }, + }, + { + name: "no-mapping-override-change-username-and-password-attributes", + opts: []vault.Option{ + vault.WithCredentialType("user_password"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField, usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "new-user", + passwordAttribute: "new-pass", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + v := map[string]interface{}{ + usernameAttribute: "new-user", + passwordAttribute: "new-pass", + } + var err error + out.CredentialMappingOverrides, err = structpb.NewStruct(v) + require.NoError(t, err) + return out + }, + }, + { + name: "user-password-attributes-delete-mapping-override", + opts: []vault.Option{ + vault.WithCredentialType("user_password"), + vault.WithMappingOverride( + vault.NewUserPasswordOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePasswordAttribute("orig-pass"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(credentialMappingPathField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: nil, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, + { + name: "no-mapping-override-delete-mapping-override", + opts: []vault.Option{ + vault.WithCredentialType("user_password"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(credentialMappingPathField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: nil, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, + { + name: "user-password-attributes-delete-mapping-override-field-specific", + opts: []vault.Option{ + vault.WithCredentialType("user_password"), + vault.WithMappingOverride( + vault.NewUserPasswordOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePasswordAttribute("orig-pass"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField, usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: nil, + passwordAttribute: nil, + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, + { + name: "no-mapping-override-delete-mapping-override-field-specific", + opts: []vault.Option{ + vault.WithCredentialType("user_password"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField, usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: nil, + passwordAttribute: nil, + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, } for _, tc := range successCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - st, cleanup := freshLibrary() + st, cleanup := freshLibrary(tc.opts...) defer cleanup() if tc.req.Item.GetVersion() == 0 { @@ -969,6 +1241,11 @@ func TestUpdate(t *testing.T) { path: "authorized actions", item: &pb.CredentialLibrary{AuthorizedActions: append(testAuthorizedActions, "another")}, }, + { + name: "read only credential type", + path: "credential_type", + item: &pb.CredentialLibrary{CredentialType: string(credential.UserPasswordType)}, + }, } for _, tc := range errCases { t.Run(tc.name, func(t *testing.T) {