From c317901db4aee9502bf18be037d969d0f6f71b52 Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Mon, 11 Oct 2021 16:16:02 -0400 Subject: [PATCH 01/12] feat(credential): add credential type to credential library on create This change adds a credential type attribute to credential library and allows it to be set when creating a credential library. Specifying the type of credential returned from a library allows for new features and additional error checking to be added over time. A User Password credential type and an Unspecified credential type are included with the expectation that additional credential types will be added in the future. This change also adds the credential type attribute to existing credential libraries and sets it to Unspecified. Known Limitations: - Credential type is only supported on the repository create method. Support for the other CRUD methods will be added in the future. --- internal/credential/credential.go | 12 + internal/credential/store/credential.pb.go | 47 ++-- .../credential/vault/credential_library.go | 42 ++- .../vault/credential_library_test.go | 52 +++- .../credential/vault/mapping_overriders.go | 98 +++++++ .../vault/mapping_overriders_test.go | 68 +++++ internal/credential/vault/options.go | 59 ++++- internal/credential/vault/options_test.go | 25 ++ .../vault/repository_credential_library.go | 33 ++- .../repository_credential_library_test.go | 86 ++++++ internal/credential/vault/store/vault.pb.go | 245 +++++++++++++----- .../oss/postgres/22/01_sentinel.up.sql | 17 ++ .../oss/postgres/22/02_credential_type.up.sql | 77 ++++++ .../postgres/22/03_vault_library_map.up.sql | 40 +++ .../04_vault_library_map_user_password.up.sql | 32 +++ internal/db/sqltest/Makefile | 4 +- .../sqltest/initdb.d/03_widgets_persona.sql | 32 ++- .../vault/credential_vault_library.sql | 39 +++ ...library_user_password_mapping_override.sql | 44 ++++ .../db/sqltest/tests/sentinel/is_sentinel.sql | 21 ++ .../db/sqltest/tests/sentinel/to_sentinel.sql | 21 ++ internal/errors/code.go | 1 + internal/errors/code_test.go | 5 + internal/errors/info.go | 4 + .../credential/store/v1/credential.proto | 5 + .../credential/vault/store/v1/vault.proto | 31 +++ 26 files changed, 1033 insertions(+), 107 deletions(-) create mode 100644 internal/credential/vault/mapping_overriders.go create mode 100644 internal/credential/vault/mapping_overriders_test.go create mode 100644 internal/db/schema/migrations/oss/postgres/22/01_sentinel.up.sql create mode 100644 internal/db/schema/migrations/oss/postgres/22/02_credential_type.up.sql create mode 100644 internal/db/schema/migrations/oss/postgres/22/03_vault_library_map.up.sql create mode 100644 internal/db/schema/migrations/oss/postgres/22/04_vault_library_map_user_password.up.sql create mode 100644 internal/db/sqltest/tests/credential/vault/credential_vault_library.sql create mode 100644 internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql create mode 100644 internal/db/sqltest/tests/sentinel/is_sentinel.sql create mode 100644 internal/db/sqltest/tests/sentinel/to_sentinel.sql diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 5c337ae21..1029448fe 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -18,11 +18,23 @@ type Store interface { GetScopeId() string } +// Type is the type of credential provided by a library. +type Type string + +// Credential type values. +const ( + UnspecifiedType Type = "unspecified" + UserPasswordType Type = "user_password" +) + // A Library is a resource that provides credentials that are of the same // type and access level from a single store. type Library interface { boundary.Resource GetStoreId() string + + // TODO(mgaffney) 10/2021: Add method for returning the credential type + // of the library } // Purpose is the purpose of the credential. diff --git a/internal/credential/store/credential.pb.go b/internal/credential/store/credential.pb.go index 6f99c3d0c..dc63aa23c 100644 --- a/internal/credential/store/credential.pb.go +++ b/internal/credential/store/credential.pb.go @@ -92,6 +92,10 @@ type Library struct { // The store_id of the owning store and must be set. // @inject_tag: `gorm:"not_null"` StoreId string `protobuf:"bytes,2,opt,name=store_id,json=storeId,proto3" json:"store_id,omitempty" gorm:"not_null"` + // credential_type is optional. If set, it indicates the type of + // credential the library returns. + // @inject_tag: `gorm:"default:null"` + CredentialType string `protobuf:"bytes,3,opt,name=credential_type,json=credentialType,proto3" json:"credential_type,omitempty" gorm:"default:null"` } func (x *Library) Reset() { @@ -140,6 +144,13 @@ func (x *Library) GetStoreId() string { return "" } +func (x *Library) GetCredentialType() string { + if x != nil { + return x.CredentialType + } + return "" +} + type Credential struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -320,27 +331,29 @@ var file_controller_storage_credential_store_v1_credential_proto_rawDesc = []byt 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, - 0x49, 0x64, 0x22, 0x41, 0x0a, 0x07, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, + 0x49, 0x64, 0x22, 0x6a, 0x0a, 0x07, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x49, 0x64, 0x22, 0x29, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, + 0x6f, 0x72, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x22, 0x29, + 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x22, 0x40, 0x0a, 0x06, 0x53, 0x74, 0x61, + 0x74, 0x69, 0x63, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, - 0x22, 0x40, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x49, 0x64, 0x22, 0x45, 0x0a, 0x07, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x1b, 0x0a, - 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, - 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2f, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x64, 0x22, 0x45, 0x0a, 0x07, 0x44, + 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, + 0x49, 0x64, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/credential/vault/credential_library.go b/internal/credential/vault/credential_library.go index 6de708262..0ad28e091 100644 --- a/internal/credential/vault/credential_library.go +++ b/internal/credential/vault/credential_library.go @@ -1,8 +1,11 @@ package vault import ( + "context" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/store" + "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/oplog" "google.golang.org/protobuf/proto" ) @@ -21,17 +24,20 @@ const ( type CredentialLibrary struct { *store.CredentialLibrary tableName string `gorm:"-"` + + MappingOverride MappingOverride `gorm:"-"` } // NewCredentialLibrary creates a new in memory CredentialLibrary // for a Vault backend at vaultPath assigned to storeId. -// Name, description, method, and request body are the only valid options. -// All other options are ignored. +// Name, description, method, request body, credential type, and mapping +// override are the only valid options. All other options are ignored. func NewCredentialLibrary(storeId string, vaultPath string, opt ...Option) (*CredentialLibrary, error) { const op = "vault.NewCredentialLibrary" opts := getOpts(opt...) l := &CredentialLibrary{ + MappingOverride: opts.withMappingOverride, CredentialLibrary: &store.CredentialLibrary{ StoreId: storeId, Name: opts.withName, @@ -39,12 +45,21 @@ func NewCredentialLibrary(storeId string, vaultPath string, opt ...Option) (*Cre VaultPath: vaultPath, HttpRequestBody: opts.withRequestBody, HttpMethod: string(opts.withMethod), + CredentialType: string(opts.withCredentialType), }, } return l, nil } +func (l *CredentialLibrary) validate(ctx context.Context, caller errors.Op) error { + switch { + case !validMappingOverride(l.MappingOverride, l.CredentialType()): + return errors.New(ctx, errors.VaultInvalidMappingOverride, caller, "invalid credential type for mapping override") + } + return nil +} + func allocCredentialLibrary() *CredentialLibrary { return &CredentialLibrary{ CredentialLibrary: &store.CredentialLibrary{}, @@ -52,12 +67,25 @@ func allocCredentialLibrary() *CredentialLibrary { } func (l *CredentialLibrary) clone() *CredentialLibrary { + var m MappingOverride + if l.MappingOverride != nil { + m = l.MappingOverride.clone() + } + cp := proto.Clone(l.CredentialLibrary) return &CredentialLibrary{ + MappingOverride: m, CredentialLibrary: cp.(*store.CredentialLibrary), } } +func (l *CredentialLibrary) setId(i string) { + l.PublicId = i + if l.MappingOverride != nil { + l.MappingOverride.setLibraryId(i) + } +} + // TableName returns the table name. func (l *CredentialLibrary) TableName() string { if l.tableName != "" { @@ -83,4 +111,14 @@ func (l *CredentialLibrary) oplog(op oplog.OpType) oplog.Metadata { return metadata } +// CredentialType returns the type of credential the library retrieves. +func (l *CredentialLibrary) CredentialType() credential.Type { + switch ct := l.GetCredentialType(); ct { + case "": + return credential.UnspecifiedType + default: + return credential.Type(ct) + } +} + var _ credential.Library = (*CredentialLibrary)(nil) diff --git a/internal/credential/vault/credential_library_test.go b/internal/credential/vault/credential_library_test.go index 5752d6bbb..32685ac8a 100644 --- a/internal/credential/vault/credential_library_test.go +++ b/internal/credential/vault/credential_library_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/store" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/iam" @@ -37,7 +38,6 @@ func TestCredentialLibrary_New(t *testing.T) { name string args args want *CredentialLibrary - wantErr bool wantCreateErr bool }{ { @@ -190,6 +190,42 @@ func TestCredentialLibrary_New(t *testing.T) { }, wantCreateErr: true, }, + { + name: "minimum-actually-valid", + args: args{ + storeId: cs.PublicId, + vaultPath: "vault/path", + opts: []Option{ + WithMethod(MethodGet), + }, + }, + want: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.PublicId, + VaultPath: "vault/path", + HttpMethod: string(MethodGet), + }, + }, + }, + { + name: "credential-type", + args: args{ + storeId: cs.PublicId, + vaultPath: "vault/path", + opts: []Option{ + WithMethod(MethodGet), + WithCredentialType(credential.UserPasswordType), + }, + }, + want: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.PublicId, + VaultPath: "vault/path", + HttpMethod: string(MethodGet), + CredentialType: string(credential.UserPasswordType), + }, + }, + }, } for _, tt := range tests { @@ -198,17 +234,21 @@ func TestCredentialLibrary_New(t *testing.T) { assert, require := assert.New(t), require.New(t) ctx := context.Background() got, err := NewCredentialLibrary(tt.args.storeId, tt.args.vaultPath, tt.args.opts...) - if tt.wantErr { - assert.Error(err) - require.Nil(got) - return - } require.NoError(err) require.NotNil(got) assert.Emptyf(got.PublicId, "PublicId set") assert.Equal(tt.want, got) + switch ct := tt.want.GetCredentialType(); ct { + case string(credential.UserPasswordType): + assert.Equal(credential.UserPasswordType, got.CredentialType()) + case string(credential.UnspecifiedType), "": + assert.Equal(credential.UnspecifiedType, got.CredentialType()) + default: + assert.Failf("Unknown credential type", "%s", ct) + } + id, err := newCredentialLibraryId() assert.NoError(err) diff --git a/internal/credential/vault/mapping_overriders.go b/internal/credential/vault/mapping_overriders.go new file mode 100644 index 000000000..cf2b4d889 --- /dev/null +++ b/internal/credential/vault/mapping_overriders.go @@ -0,0 +1,98 @@ +package vault + +import ( + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault/store" + "github.com/hashicorp/boundary/internal/db/sanitize" + "github.com/hashicorp/boundary/internal/db/sentinel" + "google.golang.org/protobuf/proto" +) + +// validMappingOverride reports whether the given mapping override is valid +// for the given credential type. +func validMappingOverride(m MappingOverride, ct credential.Type) bool { + switch m.(type) { + case nil: + return true // it is always valid to not specify a mapping override + case *UserPasswordOverride: + return ct == credential.UserPasswordType + default: + return false // an unknown mapping override type is never valid + } +} + +// A MappingOverride is an interface holding one of the mapping override +// types: UserPasswordOverride. +type MappingOverride interface { + clone() MappingOverride + setLibraryId(i string) + + // sanitize replaces all sentinel values in the MappingOverride with + // zero values. It is called before a MappingOverride is returned from + // the domain layer when the MappingOverride has been loaded from the + // database. + sanitize() +} + +// A UserPasswordOverride contains optional values for overriding the +// default mappings used to map a Vault secret to a UserPassword credential +// type for the credential library that owns it. +type UserPasswordOverride struct { + *store.UserPasswordOverride + tableName string `gorm:"-"` +} + +var _ MappingOverride = (*UserPasswordOverride)(nil) + +// NewUserPasswordOverride creates a new in memory UserPasswordOverride. +// WithOverrideUsernameAttribute and WithOverridePasswordAttribute are the +// only valid options. All other options are ignored. +func NewUserPasswordOverride(opt ...Option) *UserPasswordOverride { + opts := getOpts(opt...) + o := &UserPasswordOverride{ + UserPasswordOverride: &store.UserPasswordOverride{ + UsernameAttribute: sanitize.String(opts.withOverrideUsernameAttribute), + PasswordAttribute: sanitize.String(opts.withOverridePasswordAttribute), + }, + } + return o +} + +func allocUserPasswordOverride() *UserPasswordOverride { + return &UserPasswordOverride{ + UserPasswordOverride: &store.UserPasswordOverride{}, + } +} + +func (o *UserPasswordOverride) clone() MappingOverride { + cp := proto.Clone(o.UserPasswordOverride) + return &UserPasswordOverride{ + UserPasswordOverride: cp.(*store.UserPasswordOverride), + } +} + +func (o *UserPasswordOverride) setLibraryId(i string) { + o.LibraryId = i +} + +func (o *UserPasswordOverride) sanitize() { + if sentinel.Is(o.UsernameAttribute) { + o.UsernameAttribute = "" + } + if sentinel.Is(o.PasswordAttribute) { + o.PasswordAttribute = "" + } +} + +// TableName returns the table name. +func (o *UserPasswordOverride) TableName() string { + if o.tableName != "" { + return o.tableName + } + return "credential_vault_library_user_password_mapping_override" +} + +// SetTableName sets the table name. +func (o *UserPasswordOverride) SetTableName(n string) { + o.tableName = n +} diff --git a/internal/credential/vault/mapping_overriders_test.go b/internal/credential/vault/mapping_overriders_test.go new file mode 100644 index 000000000..735645ed1 --- /dev/null +++ b/internal/credential/vault/mapping_overriders_test.go @@ -0,0 +1,68 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/stretchr/testify/assert" +) + +type unknownMapper int + +func (u unknownMapper) clone() MappingOverride { return u } +func (u unknownMapper) setLibraryId(_ string) {} +func (u unknownMapper) sanitize() {} + +var _ MappingOverride = unknownMapper(0) + +func TestValidMappingOverrides(t *testing.T) { + tests := []struct { + m MappingOverride + ct credential.Type + want bool + }{ + { + m: nil, + ct: "invalid", + want: true, + }, + { + m: nil, + ct: credential.UnspecifiedType, + want: true, + }, + { + m: nil, + ct: credential.UserPasswordType, + want: true, + }, + { + m: unknownMapper(1), + ct: credential.UnspecifiedType, + want: false, + }, + { + m: unknownMapper(1), + ct: credential.UserPasswordType, + want: false, + }, + { + m: allocUserPasswordOverride(), + ct: credential.UnspecifiedType, + want: false, + }, + { + m: allocUserPasswordOverride(), + ct: credential.UserPasswordType, + want: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("%T-%s", tt.m, tt.ct), func(t *testing.T) { + got := validMappingOverride(tt.m, tt.ct) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/credential/vault/options.go b/internal/credential/vault/options.go index 143cbe621..015dea7ba 100644 --- a/internal/credential/vault/options.go +++ b/internal/credential/vault/options.go @@ -1,5 +1,7 @@ package vault +import "github.com/hashicorp/boundary/internal/credential" + // getOpts - iterate the inbound Options and return a struct func getOpts(opt ...Option) options { opts := getDefaultOptions() @@ -14,16 +16,21 @@ type Option func(*options) // options = how options are represented type options struct { - withName string - withDescription string - withLimit int - withCACert []byte - withNamespace string - withTlsServerName string - withTlsSkipVerify bool - withClientCert *ClientCertificate - withMethod Method - withRequestBody []byte + withName string + withDescription string + withLimit int + withCACert []byte + withNamespace string + withTlsServerName string + withTlsSkipVerify bool + withClientCert *ClientCertificate + withMethod Method + withRequestBody []byte + withCredentialType credential.Type + + withOverrideUsernameAttribute string + withOverridePasswordAttribute string + withMappingOverride MappingOverride } func getDefaultOptions() options { @@ -109,3 +116,35 @@ func WithRequestBody(b []byte) Option { o.withRequestBody = b } } + +// WithCredentialType provides an optional credential type to associate +// with a credential library. +func WithCredentialType(t credential.Type) Option { + return func(o *options) { + o.withCredentialType = t + } +} + +// WithOverrideUsernameAttribute provides the name of an attribute in the +// Data field of a Vault api.Secret that maps to a username value. +func WithOverrideUsernameAttribute(s string) Option { + return func(o *options) { + o.withOverrideUsernameAttribute = s + } +} + +// WithOverridePasswordAttribute provides the name of an attribute in the +// Data field of a Vault api.Secret that maps to a password value. +func WithOverridePasswordAttribute(s string) Option { + return func(o *options) { + o.withOverridePasswordAttribute = s + } +} + +// WithMappingOverride provides an optional mapping override to use for +// mapping the Data fields of a Vault api.Secret to a credential. +func WithMappingOverride(m MappingOverride) Option { + return func(o *options) { + o.withMappingOverride = m + } +} diff --git a/internal/credential/vault/options_test.go b/internal/credential/vault/options_test.go index b4f780109..eabbb98bb 100644 --- a/internal/credential/vault/options_test.go +++ b/internal/credential/vault/options_test.go @@ -3,6 +3,7 @@ package vault import ( "testing" + "github.com/hashicorp/boundary/internal/credential" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -83,4 +84,28 @@ func Test_GetOpts(t *testing.T) { testOpts.withRequestBody = []byte("body") assert.Equal(t, opts, testOpts) }) + t.Run("WithCredentialType", func(t *testing.T) { + opts := getOpts(WithCredentialType(credential.UserPasswordType)) + testOpts := getDefaultOptions() + testOpts.withCredentialType = credential.UserPasswordType + assert.Equal(t, opts, testOpts) + }) + t.Run("WithOverrideUsernameAttribute", func(t *testing.T) { + opts := getOpts(WithOverrideUsernameAttribute("test")) + testOpts := getDefaultOptions() + testOpts.withOverrideUsernameAttribute = "test" + assert.Equal(t, opts, testOpts) + }) + t.Run("WithOverridePasswordAttribute", func(t *testing.T) { + opts := getOpts(WithOverridePasswordAttribute("test")) + testOpts := getDefaultOptions() + testOpts.withOverridePasswordAttribute = "test" + assert.Equal(t, opts, testOpts) + }) + t.Run("WithMappingOverride", func(t *testing.T) { + opts := getOpts(WithMappingOverride(unknownMapper(1))) + testOpts := getDefaultOptions() + testOpts.withMappingOverride = unknownMapper(1) + assert.Equal(t, opts, testOpts) + }) } diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index d28727664..6d2c763cd 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -48,11 +48,15 @@ func (r *Repository) CreateCredentialLibrary(ctx context.Context, scopeId string l.HttpMethod = string(MethodGet) } + if err := l.validate(ctx, op); err != nil { + return nil, err // intentionally not wrapped. + } + id, err := newCredentialLibraryId() if err != nil { return nil, errors.Wrap(ctx, err, op) } - l.PublicId = id + l.setId(id) oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) if err != nil { @@ -62,11 +66,34 @@ func (r *Repository) CreateCredentialLibrary(ctx context.Context, scopeId string var newCredentialLibrary *CredentialLibrary _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error { - newCredentialLibrary = l.clone() - err := w.Create(ctx, newCredentialLibrary, db.WithOplog(oplogWrapper, l.oplog(oplog.OpType_OP_TYPE_CREATE))) + var msgs []*oplog.Message + ticket, err := w.GetTicket(l) if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) + } + + // insert credential library + newCredentialLibrary = l.clone() + var lOplogMsg oplog.Message + if err := w.Create(ctx, newCredentialLibrary, db.NewOplogMsg(&lOplogMsg)); err != nil { return errors.Wrap(ctx, err, op) } + msgs = append(msgs, &lOplogMsg) + + // insert mapper override (if exists) + if l.MappingOverride != nil { + var msg oplog.Message + if err := w.Create(ctx, newCredentialLibrary.MappingOverride, db.NewOplogMsg(&msg)); err != nil { + return errors.Wrap(ctx, err, op) + } + newCredentialLibrary.MappingOverride.sanitize() + msgs = append(msgs, &msg) + } + + metadata := l.oplog(oplog.OpType_OP_TYPE_CREATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } return nil }, ) diff --git a/internal/credential/vault/repository_credential_library_test.go b/internal/credential/vault/repository_credential_library_test.go index 039675242..a3caaf27d 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/store" "github.com/hashicorp/boundary/internal/db" dbassert "github.com/hashicorp/boundary/internal/db/assert" @@ -166,6 +167,71 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { }, }, }, + { + name: "valid-user-password-credential-type", + in: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + want: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, + { + name: "unknown-mapping-override-type", + in: &CredentialLibrary{ + MappingOverride: unknownMapper(1), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + wantErr: errors.VaultInvalidMappingOverride, + }, + { + name: "invalid-mapping-override-type", + in: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride(WithOverrideUsernameAttribute("test")), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + }, + }, + wantErr: errors.VaultInvalidMappingOverride, + }, + { + name: "valid-user-password-credential-type-with-override", + in: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride(WithOverrideUsernameAttribute("test")), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride(WithOverrideUsernameAttribute("test")), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, } for _, tt := range tests { @@ -191,8 +257,28 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { assert.NotSame(tt.in, got) assert.Equal(tt.want.Name, got.Name) assert.Equal(tt.want.Description, got.Description) + assert.Equal(tt.want.CredentialType(), got.CredentialType()) assert.Equal(got.CreateTime, got.UpdateTime) + assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + + if tt.in.MappingOverride != nil { + require.NotNil(got.MappingOverride) + assert.IsType(tt.want.MappingOverride, got.MappingOverride) + switch w := tt.want.MappingOverride.(type) { + case *UserPasswordOverride: + g, ok := got.MappingOverride.(*UserPasswordOverride) + require.True(ok) + assert.Equal(w.UsernameAttribute, g.UsernameAttribute) + assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + default: + assert.Fail("Unknown mapping override") + } + + // verify it was persisted in the database + override := allocUserPasswordOverride() + assert.NoError(rw.LookupWhere(ctx, &override, "library_id = ?", got.GetPublicId())) + } }) } diff --git a/internal/credential/vault/store/vault.pb.go b/internal/credential/vault/store/vault.pb.go index 14693ded2..6bbdc9552 100644 --- a/internal/credential/vault/store/vault.pb.go +++ b/internal/credential/vault/store/vault.pb.go @@ -502,6 +502,10 @@ type CredentialLibrary struct { // Can only be set if http_method is POST. // @inject_tag: `gorm:"default:null"` HttpRequestBody []byte `protobuf:"bytes,10,opt,name=http_request_body,json=httpRequestBody,proto3" json:"http_request_body,omitempty" gorm:"default:null"` + // credential_type is optional. If set, it indicates the type of + // credential the library returns. + // @inject_tag: `gorm:"default:null"` + CredentialType string `protobuf:"bytes,11,opt,name=credential_type,json=credentialType,proto3" json:"credential_type,omitempty" gorm:"default:null"` } func (x *CredentialLibrary) Reset() { @@ -606,6 +610,13 @@ func (x *CredentialLibrary) GetHttpRequestBody() []byte { return nil } +func (x *CredentialLibrary) GetCredentialType() string { + if x != nil { + return x.CredentialType + } + return "" +} + type Credential struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -776,6 +787,87 @@ func (x *Credential) GetStatus() string { return "" } +type UserPasswordOverride struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // library_id of the owning vault credential library. + // @inject_tag: `gorm:"primary_key"` + LibraryId string `protobuf:"bytes,1,opt,name=library_id,json=libraryId,proto3" json:"library_id,omitempty" gorm:"primary_key"` + // username_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a username. + // If set, it overrides any default attribute names the system uses to + // find a username attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + UsernameAttribute string `protobuf:"bytes,2,opt,name=username_attribute,json=usernameAttribute,proto3" json:"username_attribute,omitempty" gorm:"default:null"` + // password_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a password. + // If set, it overrides any default attribute names the system uses to + // find a password attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + PasswordAttribute string `protobuf:"bytes,3,opt,name=password_attribute,json=passwordAttribute,proto3" json:"password_attribute,omitempty" gorm:"default:null"` +} + +func (x *UserPasswordOverride) Reset() { + *x = UserPasswordOverride{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserPasswordOverride) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserPasswordOverride) ProtoMessage() {} + +func (x *UserPasswordOverride) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserPasswordOverride.ProtoReflect.Descriptor instead. +func (*UserPasswordOverride) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{5} +} + +func (x *UserPasswordOverride) GetLibraryId() string { + if x != nil { + return x.LibraryId + } + return "" +} + +func (x *UserPasswordOverride) GetUsernameAttribute() string { + if x != nil { + return x.UsernameAttribute + } + return "" +} + +func (x *UserPasswordOverride) GetPasswordAttribute() string { + if x != nil { + return x.PasswordAttribute + } + return "" +} + var File_controller_storage_credential_vault_store_v1_vault_proto protoreflect.FileDescriptor var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []byte{ @@ -897,7 +989,7 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []by 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, - 0x49, 0x64, 0x22, 0xd4, 0x04, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x49, 0x64, 0x22, 0xfd, 0x04, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, @@ -934,48 +1026,60 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []by 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x52, 0x0f, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x22, 0xc3, 0x04, 0x0a, 0x0a, 0x43, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, - 0x72, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x68, 0x6d, 0x61, - 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x48, 0x6d, - 0x61, 0x63, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x56, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, - 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, - 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x53, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, - 0x61, 0x62, 0x6c, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x52, 0x65, - 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, - 0x45, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, - 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, + 0x70, 0x65, 0x22, 0xc3, 0x04, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x1d, + 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x4b, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, + 0x12, 0x56, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x6e, + 0x65, 0x77, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x53, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x0a, + 0x0c, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x14, 0x55, 0x73, 0x65, + 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, + 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, + 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, + 0x2d, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x42, 0x45, + 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -990,29 +1094,30 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP() return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescData } -var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_controller_storage_credential_vault_store_v1_vault_proto_goTypes = []interface{}{ - (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore - (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token - (*ClientCertificate)(nil), // 2: controller.storage.credential.vault.store.v1.ClientCertificate - (*CredentialLibrary)(nil), // 3: controller.storage.credential.vault.store.v1.CredentialLibrary - (*Credential)(nil), // 4: controller.storage.credential.vault.store.v1.Credential - (*timestamp.Timestamp)(nil), // 5: controller.storage.timestamp.v1.Timestamp + (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore + (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token + (*ClientCertificate)(nil), // 2: controller.storage.credential.vault.store.v1.ClientCertificate + (*CredentialLibrary)(nil), // 3: controller.storage.credential.vault.store.v1.CredentialLibrary + (*Credential)(nil), // 4: controller.storage.credential.vault.store.v1.Credential + (*UserPasswordOverride)(nil), // 5: controller.storage.credential.vault.store.v1.UserPasswordOverride + (*timestamp.Timestamp)(nil), // 6: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_vault_store_v1_vault_proto_depIdxs = []int32{ - 5, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 9: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 10: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 11: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 12: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 9: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 10: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 11: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 12: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp 13, // [13:13] is the sub-list for method output_type 13, // [13:13] is the sub-list for method input_type 13, // [13:13] is the sub-list for extension type_name @@ -1086,6 +1191,18 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { return nil } } + file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserPasswordOverride); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1093,7 +1210,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/db/schema/migrations/oss/postgres/22/01_sentinel.up.sql b/internal/db/schema/migrations/oss/postgres/22/01_sentinel.up.sql new file mode 100644 index 000000000..c90f520ea --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/22/01_sentinel.up.sql @@ -0,0 +1,17 @@ +begin; + + create function wt_to_sentinel(string text) + returns text + as $$ + select concat( + u&'\fffe', + trim(trailing u&'\ffff' from trim(leading u&'\fffe ' from string)), + u&'\ffff' + ); + $$ language sql + immutable + returns null on null input; + comment on function wt_to_sentinel is + 'wt_to_sentinel takes a string and returns it as a wt_sentinel'; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/22/02_credential_type.up.sql b/internal/db/schema/migrations/oss/postgres/22/02_credential_type.up.sql new file mode 100644 index 000000000..3bd027cc3 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/22/02_credential_type.up.sql @@ -0,0 +1,77 @@ +begin; + + create table credential_type_enm ( + name text primary key + constraint only_predefined_credential_types_allowed + check ( + name in ( + 'unspecified', + 'user_password' + ) + ) + ); + comment on table credential_type_enm is + 'credential_type_enm is an enumeration table for the types of credentials a credential library can provide.'; + + insert into credential_type_enm (name) + values + ('unspecified'), + ('user_password'); + + -- Add a credential_type column to the base credential_library table and the + -- credential_vault_library subtype table. + + alter table credential_library + add column credential_type text not null default 'unspecified' + constraint credential_type_enm_fkey + references credential_type_enm (name) + on delete restrict + on update cascade, + add constraint credential_library_store_id_public_id_credential_type_uq + unique(store_id, public_id, credential_type) + ; + + alter table credential_vault_library + add column credential_type text not null default 'unspecified' + constraint credential_type_enm_fkey + references credential_type_enm (name) + on delete restrict + on update cascade, + drop constraint credential_library_fkey, + add constraint credential_library_fkey + foreign key (store_id, public_id, credential_type) + references credential_library (store_id, public_id, credential_type) + on delete cascade + on update cascade + ; + + -- This drops a constraint that is no longer needed. + -- It could not be dropped until after the credential_library_fkey constraint was + -- dropped and then recreated. + alter table credential_library + drop constraint credential_library_store_id_public_id_uq + ; + + -- Drop and recreate the insert trigger function for credential_library subtypes. + + drop trigger insert_credential_library_subtype on credential_vault_library; + drop function insert_credential_library_subtype(); + + -- insert_credential_library_subtype() is a before insert trigger + -- function for subtypes of credential_library + create function insert_credential_library_subtype() + returns trigger + as $$ + begin + insert into credential_library + (public_id, store_id, credential_type) + values + (new.public_id, new.store_id, new.credential_type); + return new; + end; + $$ language plpgsql; + + create trigger insert_credential_library_subtype before insert on credential_vault_library + for each row execute procedure insert_credential_library_subtype(); + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/22/03_vault_library_map.up.sql b/internal/db/schema/migrations/oss/postgres/22/03_vault_library_map.up.sql new file mode 100644 index 000000000..77cfc0570 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/22/03_vault_library_map.up.sql @@ -0,0 +1,40 @@ +begin; + + create table credential_vault_library_mapping_override ( + library_id wt_public_id primary key + constraint credential_vault_library_fkey + references credential_vault_library (public_id) + on delete cascade + on update cascade + ); + comment on table credential_vault_library_mapping_override is + 'credential_vault_library_mapping_override is a base table for the vault library mapping override type. ' + 'Each row is owned by a single vault library and maps 1-to-1 to a row in one of the vault library mapping override subtype tables.'; + + -- insert_credential_vault_library_mapping_override_subtype() is a before insert trigger + -- function for subtypes of credential_vault_library_mapping_override + create function insert_credential_vault_library_mapping_override_subtype() + returns trigger + as $$ + begin + insert into credential_vault_library_mapping_override + (library_id) + values + (new.library_id); + return new; + end; + $$ language plpgsql; + + -- delete_credential_vault_library_mapping_override_subtype() is an after delete trigger + -- function for subtypes of credential_vault_library_mapping_override + create function delete_credential_vault_library_mapping_override_subtype() + returns trigger + as $$ + begin + delete from credential_vault_library_mapping_override + where library_id = old.library_id; + return null; -- result is ignored since this is an after trigger + end; + $$ language plpgsql; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/22/04_vault_library_map_user_password.up.sql b/internal/db/schema/migrations/oss/postgres/22/04_vault_library_map_user_password.up.sql new file mode 100644 index 000000000..d5b8b5f45 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/22/04_vault_library_map_user_password.up.sql @@ -0,0 +1,32 @@ +begin; + + create table credential_vault_library_user_password_mapping_override ( + library_id wt_public_id primary key + constraint credential_vault_library_fkey + references credential_vault_library (public_id) + on delete cascade + on update cascade + constraint credential_vault_library_mapping_override_fkey + references credential_vault_library_mapping_override (library_id) + on delete cascade + on update cascade, + username_attribute wt_sentinel + default wt_to_sentinel('no override') + not null, + password_attribute wt_sentinel + default wt_to_sentinel('no override') + not null + ); + comment on table credential_vault_library_user_password_mapping_override is + 'credential_vault_library_user_password_mapping_override is a table ' + 'where each row represents a mapping that overrides the default mapping ' + 'from a generic vault secret to a user password credential type ' + 'for a vault credential library.'; + + create trigger insert_credential_vault_library_mapping_override_subtype before insert on credential_vault_library_user_password_mapping_override + for each row execute procedure insert_credential_vault_library_mapping_override_subtype(); + + create trigger delete_credential_vault_library_mapping_override_subtype after delete on credential_vault_library_user_password_mapping_override + for each row execute procedure delete_credential_vault_library_mapping_override_subtype(); + +commit; diff --git a/internal/db/sqltest/Makefile b/internal/db/sqltest/Makefile index a6228861c..81dbffa29 100644 --- a/internal/db/sqltest/Makefile +++ b/internal/db/sqltest/Makefile @@ -18,7 +18,9 @@ endif PROVE_OPTS ?= TESTS ?= tests/setup/*.sql \ - tests/wh/*/*.sql + tests/wh/*/*.sql \ + tests/sentinel/*.sql \ + tests/credential/*/*.sql POSTGRES_DOCKER_IMAGE_BASE ?= postgres diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 791f011e2..50e092b28 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -314,11 +314,35 @@ begin; ('p____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default'); insert into credential_vault_library - (store_id, public_id, name, description, vault_path, http_method) + (store_id, public_id, name, description, vault_path, http_method, credential_type) values - ('vs_______wvs', 'vl______wvl1', 'widget vault library', 'None', '/secrets', 'GET'), - ('vs_______wvs', 'vl______wvl2', 'widget vault ssh', 'None', '/secrets/ssh/admin', 'GET'), - ('vs_______wvs', 'vl______wvl3', 'widget vault kv', 'None', '/secrets/kv', 'GET'); + ('vs_______wvs', 'vl______wvl1', 'widget vault library', 'None', '/secrets', 'GET', 'unspecified'), + ('vs_______wvs', 'vl______wvl2', 'widget vault ssh', 'None', '/secrets/ssh/admin', 'GET', 'unspecified'), + ('vs_______wvs', 'vl______wvl3', 'widget vault kv one', 'None', '/secrets/kv/one', 'GET', 'user_password'), + ('vs_______wvs', 'vl______wvl4', 'widget vault kv two', 'None', '/secrets/kv/two', 'GET', 'user_password'), + ('vs_______wvs', 'vl______wvl5', 'widget vault kv three', 'None', '/secrets/kv/three', 'GET', 'user_password'), + ('vs_______wvs', 'vl______wvl6', 'widget vault kv four', 'None', '/secrets/kv/four', 'GET', 'user_password'), + ('vs_______wvs', 'vl______wvl7', 'widget vault kv five', 'None', '/secrets/kv/five', 'GET', 'user_password'); + + insert into credential_vault_library_user_password_mapping_override + (library_id) + values + ('vl______wvl4'); + + insert into credential_vault_library_user_password_mapping_override + (library_id, username_attribute) + values + ('vl______wvl5', 'my_username'); + + insert into credential_vault_library_user_password_mapping_override + (library_id, password_attribute) + values + ('vl______wvl6', 'my_password'); + + insert into credential_vault_library_user_password_mapping_override + (library_id, username_attribute, password_attribute) + values + ('vl______wvl7', 'my_username', 'my_password'); insert into target_credential_library (target_id, credential_library_id, credential_purpose) diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library.sql new file mode 100644 index 000000000..566dc0a4e --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library.sql @@ -0,0 +1,39 @@ +-- credential_vault_library tests: +-- the following triggers +-- insert_credential_library_subtype +-- delete_credential_library_subtype + +begin; + select plan(10); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate the setup data + select is(count(*), 1::bigint) from credential_vault_library where public_id = 'vl______wvl1' and credential_type = 'unspecified'; + select is(count(*), 1::bigint) from credential_library where public_id = 'vl______wvl1' and credential_type = 'unspecified'; + + select is(count(*), 1::bigint) from credential_vault_library where public_id = 'vl______wvl3' and credential_type = 'user_password'; + select is(count(*), 1::bigint) from credential_library where public_id = 'vl______wvl3' and credential_type = 'user_password'; + + -- validate the insert triggers + prepare insert_vault_library as + insert into credential_vault_library + (store_id, public_id, vault_path, http_method, credential_type) + values + ('vs_______wvs', 'vl_______tt1', '/secrets/kv', 'GET', 'user_password'); + select lives_ok('insert_vault_library'); + + select is(count(*), 1::bigint) from credential_vault_library where public_id = 'vl_______tt1' and credential_type = 'user_password'; + select is(count(*), 1::bigint) from credential_library where public_id = 'vl_______tt1' and credential_type = 'user_password'; + + -- validate the delete triggers + prepare delete_vault_library as + delete + from credential_vault_library + where public_id = 'vl_______tt1'; + select lives_ok('delete_vault_library'); + + select is(count(*), 0::bigint) from credential_vault_library where public_id = 'vl_______tt1'; + select is(count(*), 0::bigint) from credential_library where public_id = 'vl_______tt1'; + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql new file mode 100644 index 000000000..a0e7186b3 --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql @@ -0,0 +1,44 @@ +-- credential_vault_library_user_password_mapping_override tests: +-- the following triggers +-- insert_credential_vault_library_mapping_override_subtype +-- delete_credential_vault_library_mapping_override_subtype + +begin; + select plan(10); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate the setup data + select is(count(*), 4::bigint) + from credential_vault_library_user_password_mapping_override + where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); + + select is(count(*), 4::bigint) + from credential_vault_library_mapping_override + where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); + + -- validate the insert triggers + select is(count(*), 0::bigint) from credential_vault_library_user_password_mapping_override where library_id = 'vl______wvl3'; + select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3'; + + prepare insert_credential_vault_library_user_password_mapping_override as + insert into credential_vault_library_user_password_mapping_override + (library_id, username_attribute, password_attribute) + values + ('vl______wvl3', 'my_username', 'my_password'); + select lives_ok('insert_credential_vault_library_user_password_mapping_override'); + + select is(count(*), 1::bigint) from credential_vault_library_user_password_mapping_override where library_id = 'vl______wvl3'; + select is(count(*), 1::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3'; + + -- validate the delete triggers + prepare delete_credential_vault_library_user_password_mapping_override as + delete + from credential_vault_library_user_password_mapping_override + where library_id = 'vl______wvl3'; + select lives_ok('delete_credential_vault_library_user_password_mapping_override'); + + select is(count(*), 0::bigint) from credential_vault_library_user_password_mapping_override where library_id = 'vl______wvl3'; + select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3'; + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/sentinel/is_sentinel.sql b/internal/db/sqltest/tests/sentinel/is_sentinel.sql new file mode 100644 index 000000000..64a386b00 --- /dev/null +++ b/internal/db/sqltest/tests/sentinel/is_sentinel.sql @@ -0,0 +1,21 @@ +-- is_sentinel tests: +-- wt_is_sentinel function + +begin; + select plan(10); + + select has_domain('wt_sentinel'); + select has_function('wt_is_sentinel', array['text']); + + select ok(wt_is_sentinel(u&'\fffe' || 'default' || u&'\ffff'), 'basic sentinel'); + select ok(wt_is_sentinel(u&'\fffe' || u&'\ffff'), '0 length sentinel'); + select ok(wt_is_sentinel(u&'\fffe' || ' ' || u&'\ffff'), 'empty sentinel'); + + select ok(not wt_is_sentinel('default'), 'a word is not a sentinel'); + select ok(not wt_is_sentinel(''), '0 length string is not a sentinel'); + select ok(not wt_is_sentinel(' '), 'empty string is not a sentinel'); + select ok(not wt_is_sentinel(u&'\fffe' || 'default'), 'missing suffix of ffff is not a sentinel'); + select ok(not wt_is_sentinel('default' || u&'\ffff'), 'missing prefix of fffe is not a sentinel'); + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/sentinel/to_sentinel.sql b/internal/db/sqltest/tests/sentinel/to_sentinel.sql new file mode 100644 index 000000000..d5d2a0e59 --- /dev/null +++ b/internal/db/sqltest/tests/sentinel/to_sentinel.sql @@ -0,0 +1,21 @@ +-- to_sentinel tests: +-- wt_to_sentinel function + +begin; + select plan(10); + + select has_domain('wt_sentinel'); + select has_function('wt_to_sentinel', array['text']); + + select is(wt_to_sentinel('default'), u&'\fffe' || 'default' || u&'\ffff', 'create basic sentinel'); + select is(wt_to_sentinel(u&'\fffe' || 'default' || u&'\ffff'), u&'\fffe' || 'default' || u&'\ffff', 'no changes to existing sentinel'); + select is(wt_to_sentinel(''), u&'\fffe' || u&'\ffff', '0 length sentinel'); + select is(wt_to_sentinel(' '), u&'\fffe' || u&'\ffff', 'empty string becomes 0 length sentinel'); + select is(wt_to_sentinel(u&'\fffe' || u&'\ffff'), u&'\fffe' || u&'\ffff', '0 length sentinel to 0 length sentinel'); + select is(wt_to_sentinel(u&'\fffe'), u&'\fffe' || u&'\ffff', 'prefix to 0 length sentinel'); + select is(wt_to_sentinel(u&'\ffff'), u&'\fffe' || u&'\ffff', 'suffix to 0 length sentinel'); + + select is(wt_to_sentinel(NULL), NULL, 'null to null'); + + select * from finish(); +rollback; diff --git a/internal/errors/code.go b/internal/errors/code.go index a138528a5..f343de31b 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -108,6 +108,7 @@ const ( VaultTokenMissingCapabilities Code = 3013 // VaultTokenMissingCapabilities represents an error for a Vault token that is missing capabilities VaultCredentialRequest Code = 3014 // VaultCredentialRequest represents an error returned from Vault when retrieving a credential VaultEmptySecret Code = 3015 // VaultEmptySecret represents a empty secret was returned from Vault without error + VaultInvalidMappingOverride Code = 3016 // VaultInvalidMappingOverride represents an error returned when a credential mapping is unknown or does not match a credential type // OIDC authentication provided errors OidcProviderCallbackError Code = 4000 // OidcProviderCallbackError represents an error that is passed by the OIDC provider to the callback endpoint diff --git a/internal/errors/code_test.go b/internal/errors/code_test.go index 129a4970f..7f80a2aba 100644 --- a/internal/errors/code_test.go +++ b/internal/errors/code_test.go @@ -287,6 +287,11 @@ func TestCode_Both_String_Info(t *testing.T) { c: VaultEmptySecret, want: VaultEmptySecret, }, + { + name: "VaultInvalidMappingOverride", + c: VaultInvalidMappingOverride, + want: VaultInvalidMappingOverride, + }, { name: "OidcProviderCallbackError", c: OidcProviderCallbackError, diff --git a/internal/errors/info.go b/internal/errors/info.go index e1f317755..31e6aafda 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -232,6 +232,10 @@ var errorCodeInfo = map[Code]Info{ Message: "vault secret is empty", Kind: Integrity, }, + VaultInvalidMappingOverride: { + Message: "invalid credential mapping override", + Kind: Parameter, + }, OidcProviderCallbackError: { Message: "oidc provider callback error", Kind: External, diff --git a/internal/proto/local/controller/storage/credential/store/v1/credential.proto b/internal/proto/local/controller/storage/credential/store/v1/credential.proto index 30b0dde8e..46120b7ea 100644 --- a/internal/proto/local/controller/storage/credential/store/v1/credential.proto +++ b/internal/proto/local/controller/storage/credential/store/v1/credential.proto @@ -22,6 +22,11 @@ message Library { // The store_id of the owning store and must be set. // @inject_tag: `gorm:"not_null"` string store_id = 2; + + // credential_type is optional. If set, it indicates the type of + // credential the library returns. + // @inject_tag: `gorm:"default:null"` + string credential_type = 3; } message Credential { diff --git a/internal/proto/local/controller/storage/credential/vault/store/v1/vault.proto b/internal/proto/local/controller/storage/credential/vault/store/v1/vault.proto index 287577e20..b99fc502e 100644 --- a/internal/proto/local/controller/storage/credential/vault/store/v1/vault.proto +++ b/internal/proto/local/controller/storage/credential/vault/store/v1/vault.proto @@ -205,6 +205,11 @@ message CredentialLibrary { // Can only be set if http_method is POST. // @inject_tag: `gorm:"default:null"` bytes http_request_body = 10 [(custom_options.v1.mask_mapping) = {this:"HttpRequestBody" that: "attributes.http_request_body"}]; + + // credential_type is optional. If set, it indicates the type of + // credential the library returns. + // @inject_tag: `gorm:"default:null"` + string credential_type = 11; } message Credential { @@ -267,3 +272,29 @@ message Credential { // @inject_tag: `gorm:"not_null"` string status = 12; } + +message UserPasswordOverride { + // library_id of the owning vault credential library. + // @inject_tag: `gorm:"primary_key"` + string library_id = 1; + + // username_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a username. + // If set, it overrides any default attribute names the system uses to + // find a username attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + string username_attribute = 2; + + // password_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a password. + // If set, it overrides any default attribute names the system uses to + // find a password attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + string password_attribute = 3; +} From 1cc35fb0dfb40b80a633df1400b3a6447dbe5c58 Mon Sep 17 00:00:00 2001 From: Louis Ruch Date: Wed, 27 Oct 2021 14:16:22 -0700 Subject: [PATCH 02/12] feat: Credential library mapping API/SDK/CLI on create path * feat(sdk): Add fields to credential library proto Adds CredentialType and CredentialMappingOverrides to credential library SDK. * feat(api): Add fields to credenttial library service Adds support for CredentialType and CredentialMappingOverrides to the credential library service (API) * feat(cli): Add feilds for credential library cmd Adds support for CredentialType and CredentialMappingOverrides to the credential library cmd command. --- .../credential_library.gen.go | 24 +-- api/credentiallibraries/option.gen.go | 24 +++ globals/fields.go | 2 + .../commands/credentiallibrariescmd/funcs.go | 14 ++ .../credentiallibrariescmd/vault_funcs.go | 68 ++++++- .../vault/credential_library_test.go | 21 ++ internal/gen/controller.swagger.json | 8 + .../v1/credential_library.proto | 6 + .../credentiallibrary_service.go | 87 +++++++- .../credentiallibrary_service_test.go | 187 ++++++++++++++++++ .../credential_library.pb.go | 117 ++++++----- 11 files changed, 489 insertions(+), 69 deletions(-) diff --git a/api/credentiallibraries/credential_library.gen.go b/api/credentiallibraries/credential_library.gen.go index c9357303f..e374637c5 100644 --- a/api/credentiallibraries/credential_library.gen.go +++ b/api/credentiallibraries/credential_library.gen.go @@ -13,17 +13,19 @@ import ( ) type CredentialLibrary struct { - Id string `json:"id,omitempty"` - CredentialStoreId string `json:"credential_store_id,omitempty"` - Scope *scopes.ScopeInfo `json:"scope,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - CreatedTime time.Time `json:"created_time,omitempty"` - UpdatedTime time.Time `json:"updated_time,omitempty"` - Version uint32 `json:"version,omitempty"` - Type string `json:"type,omitempty"` - Attributes map[string]interface{} `json:"attributes,omitempty"` - AuthorizedActions []string `json:"authorized_actions,omitempty"` + Id string `json:"id,omitempty"` + CredentialStoreId string `json:"credential_store_id,omitempty"` + Scope *scopes.ScopeInfo `json:"scope,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + CreatedTime time.Time `json:"created_time,omitempty"` + UpdatedTime time.Time `json:"updated_time,omitempty"` + Version uint32 `json:"version,omitempty"` + Type string `json:"type,omitempty"` + Attributes map[string]interface{} `json:"attributes,omitempty"` + AuthorizedActions []string `json:"authorized_actions,omitempty"` + CredentialType string `json:"credential_type,omitempty"` + CredentialMappingOverrides map[string]interface{} `json:"credential_mapping_overrides,omitempty"` response *api.Response } diff --git a/api/credentiallibraries/option.gen.go b/api/credentiallibraries/option.gen.go index eb401ca59..c63590351 100644 --- a/api/credentiallibraries/option.gen.go +++ b/api/credentiallibraries/option.gen.go @@ -86,6 +86,30 @@ func DefaultAttributes() Option { } } +func WithCredentialMappingOverrides(inCredentialMappingOverrides map[string]interface{}) Option { + return func(o *options) { + o.postMap["credential_mapping_overrides"] = inCredentialMappingOverrides + } +} + +func DefaultCredentialMappingOverrides() Option { + return func(o *options) { + o.postMap["credential_mapping_overrides"] = nil + } +} + +func WithCredentialType(inCredentialType string) Option { + return func(o *options) { + o.postMap["credential_type"] = inCredentialType + } +} + +func DefaultCredentialType() Option { + return func(o *options) { + o.postMap["credential_type"] = nil + } +} + func WithDescription(inDescription string) Option { return func(o *options) { o.postMap["description"] = inDescription diff --git a/globals/fields.go b/globals/fields.go index eb4ed81bb..be429bc48 100644 --- a/globals/fields.go +++ b/globals/fields.go @@ -70,4 +70,6 @@ const ( EgressCredentialSourceIdsField = "egress_credential_source_ids" EgressCredentialSourcesField = "egress_credential_sources" ConnectionsField = "connections" + CredentialTypeField = "credential_type" + CredentialMappingOverridesField = "credential_mapping_overrides" ) diff --git a/internal/cmd/commands/credentiallibrariescmd/funcs.go b/internal/cmd/commands/credentiallibrariescmd/funcs.go index 3e73fed7d..9292a227c 100644 --- a/internal/cmd/commands/credentiallibrariescmd/funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/funcs.go @@ -168,6 +168,20 @@ func printItemTable(result api.GenericResult) string { ) } + if item.CredentialType != "" { + ret = append(ret, + "", + " Credential Type:", + fmt.Sprintf(" %v", item.CredentialType), + ) + if len(item.CredentialMappingOverrides) > 0 { + ret = append(ret, + " Credential Mapping Overrides:", + base.WrapMap(4, maxLength, item.CredentialMappingOverrides), + ) + } + } + return base.WrapForHelpText(ret) } diff --git a/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go index f93f404cc..ae783aa66 100644 --- a/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go @@ -13,15 +13,19 @@ func init() { } const ( - pathFlagName = "vault-path" - httpMethodFlagName = "vault-http-method" - httpRequestBodyFlagName = "vault-http-request-body" + pathFlagName = "vault-path" + httpMethodFlagName = "vault-http-method" + httpRequestBodyFlagName = "vault-http-request-body" + credentialTypeFlagName = "credential-type" + credentialMappingFlagName = "credential-mapping-override" ) type extraVaultCmdVars struct { - flagPath string - flagHttpMethod string - flagHttpRequestBody string + flagPath string + flagHttpMethod string + flagHttpRequestBody string + flagCredentialType string + flagCredentialMapping []base.CombinedSliceFlagValue } func extraVaultActionsFlagsMapFuncImpl() map[string][]string { @@ -30,6 +34,8 @@ func extraVaultActionsFlagsMapFuncImpl() map[string][]string { pathFlagName, httpMethodFlagName, httpRequestBodyFlagName, + credentialTypeFlagName, + credentialMappingFlagName, }, } flags["update"] = flags["create"] @@ -59,11 +65,24 @@ func extraVaultFlagsFuncImpl(c *VaultCommand, set *base.FlagSets, _ *base.FlagSe Target: &c.flagHttpRequestBody, Usage: "The http request body the library uses to communicate with vault. This can be the value itself, refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read.", }) + case credentialTypeFlagName: + f.StringVar(&base.StringVar{ + Name: credentialTypeFlagName, + Target: &c.flagCredentialType, + Usage: "The type of credential this library will issue, defaults to Unspecified.", + }) + case credentialMappingFlagName: + f.CombinationSliceVar(&base.CombinationSliceVar{ + Name: credentialMappingFlagName, + Target: &c.flagCredentialMapping, + KvSplit: true, + Usage: "The credential mapping override.", + }) } } } -func extraVaultFlagHandlingFuncImpl(c *VaultCommand, f *base.FlagSets, opts *[]credentiallibraries.Option) bool { +func extraVaultFlagHandlingFuncImpl(c *VaultCommand, _ *base.FlagSets, opts *[]credentiallibraries.Option) bool { switch c.flagPath { case "": default: @@ -84,11 +103,44 @@ func extraVaultFlagHandlingFuncImpl(c *VaultCommand, f *base.FlagSets, opts *[]c rb, _ := parseutil.ParsePath(c.flagHttpRequestBody) *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryHttpRequestBody(rb)) } + switch c.flagCredentialType { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultCredentialType()) + default: + *opts = append(*opts, credentiallibraries.WithCredentialType(c.flagCredentialType)) + } + switch len(c.flagCredentialMapping) { + case 0: + case 1: + if len(c.flagCredentialMapping[0].Keys) == 0 && c.flagCredentialMapping[0].Value == "null" { + *opts = append(*opts, credentiallibraries.DefaultCredentialMappingOverrides()) + break + } + fallthrough + default: + mappings := make(map[string]interface{}, len(c.flagCredentialMapping)) + for _, mapping := range c.flagCredentialMapping { + switch { + case len(mapping.Keys) != 1 || mapping.Keys[0] == "": + // 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 + case mapping.Value == "null": + // user provided 'key=null' indicating the field specific override should + // be cleared, set map value to nil + mappings[mapping.Keys[0]] = nil + default: + mappings[mapping.Keys[0]] = mapping.Value + } + } + *opts = append(*opts, credentiallibraries.WithCredentialMappingOverrides(mappings)) + } return true } -func (c *VaultCommand) extraVaultHelpFunc(helpMap map[string]func() string) string { +func (c *VaultCommand) extraVaultHelpFunc(_ map[string]func() string) string { var helpStr string switch c.Func { case "create": diff --git a/internal/credential/vault/credential_library_test.go b/internal/credential/vault/credential_library_test.go index 32685ac8a..c6e165b7e 100644 --- a/internal/credential/vault/credential_library_test.go +++ b/internal/credential/vault/credential_library_test.go @@ -226,6 +226,27 @@ func TestCredentialLibrary_New(t *testing.T) { }, }, }, + { + name: "credential-type-with-userpass-mapping", + args: args{ + storeId: cs.PublicId, + vaultPath: "vault/path", + opts: []Option{ + WithMethod(MethodGet), + WithCredentialType(credential.UserPasswordType), + WithMappingOverride(NewUserPasswordOverride(WithOverrideUsernameAttribute("test"))), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride(WithOverrideUsernameAttribute("test")), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.PublicId, + VaultPath: "vault/path", + HttpMethod: string(MethodGet), + CredentialType: string(credential.UserPasswordType), + }, + }, + }, } for _, tt := range tests { diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 2f7913e33..a37a637f7 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -3923,6 +3923,14 @@ }, "description": "Output only. The available actions on this resource for this user.", "readOnly": true + }, + "credential_type": { + "type": "string", + "title": "The type of credential this library will issue, defaults to Unspecified" + }, + "credential_mapping_overrides": { + "type": "object", + "title": "The credential mapping overrides" } }, "title": "CredentialLibrary contains all fields related to an Credential Library resource" diff --git a/internal/proto/local/controller/api/resources/credentiallibraries/v1/credential_library.proto b/internal/proto/local/controller/api/resources/credentiallibraries/v1/credential_library.proto index 7fca04277..65d688e04 100644 --- a/internal/proto/local/controller/api/resources/credentiallibraries/v1/credential_library.proto +++ b/internal/proto/local/controller/api/resources/credentiallibraries/v1/credential_library.proto @@ -45,6 +45,12 @@ message CredentialLibrary { // Output only. The available actions on this resource for this user. repeated string authorized_actions = 300 [json_name = "authorized_actions"]; + + // The type of credential this library will issue, defaults to Unspecified + string credential_type = 310 [json_name = "credential_type", (custom_options.v1.generate_sdk_option) = true]; + + // The credential mapping overrides + google.protobuf.Struct credential_mapping_overrides = 320 [json_name = "credential_mapping_overrides", (custom_options.v1.generate_sdk_option) = true]; } // The attributes of a vault typed Credential Library. diff --git a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go index 31dc178eb..1667e4d62 100644 --- a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -21,6 +21,7 @@ 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/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -30,6 +31,12 @@ const ( httpRequestBodyField = "attributes.http_request_body" ) +// Credential mapping override attributes +const ( + usernameAttribute string = "username_attribute" + passwordAttribute string = "password_attribute" +) + var ( maskManager handlers.MaskManager @@ -463,13 +470,36 @@ func toProto(in credential.Library, opt ...handlers.Option) (*pb.CredentialLibra if outputFields.Has(globals.AuthorizedActionsField) { out.AuthorizedActions = opts.WithAuthorizedActions } - if outputFields.Has(globals.AttributesField) { - switch credential.SubtypeFromId(in.GetPublicId()) { - case vault.Subtype: - vaultIn, ok := in.(*vault.CredentialLibrary) - if !ok { - return nil, errors.NewDeprecated(errors.Internal, op, "unable to cast to vault credential library") + switch credential.SubtypeFromId(in.GetPublicId()) { + case vault.Subtype: + vaultIn, ok := in.(*vault.CredentialLibrary) + if !ok { + return nil, errors.NewDeprecated(errors.Internal, op, "unable to cast to vault credential library") + } + + if outputFields.Has(globals.CredentialTypeField) && vaultIn.GetCredentialType() != string(credential.UnspecifiedType) { + out.CredentialType = vaultIn.GetCredentialType() + if outputFields.Has(globals.CredentialMappingOverridesField) && vaultIn.MappingOverride != nil { + m := make(map[string]interface{}) + switch mapping := vaultIn.MappingOverride.(type) { + case *vault.UserPasswordOverride: + if mapping.UsernameAttribute != "" { + m[usernameAttribute] = mapping.UsernameAttribute + } + if mapping.PasswordAttribute != "" { + m[passwordAttribute] = mapping.PasswordAttribute + } + } + if len(m) > 0 { + mp, err := structpb.NewStruct(m) + if err != nil { + return nil, errors.NewDeprecated(errors.Internal, op, "creating proto struct for mapping override") + } + out.CredentialMappingOverrides = mp + } } + } + if outputFields.Has(globals.AttributesField) { attrs := &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(vaultIn.GetVaultPath()), } @@ -511,6 +541,23 @@ func toStorageVaultLibrary(storeId string, in *pb.CredentialLibrary) (out *vault opts = append(opts, vault.WithRequestBody([]byte(attrs.GetHttpRequestBody().GetValue()))) } + credentialType := credential.Type(in.GetCredentialType()) + switch credentialType { + case credential.UserPasswordType: + opts = append(opts, vault.WithCredentialType(credentialType)) + overrides := in.CredentialMappingOverrides.AsMap() + var mapOpts []vault.Option + if username := overrides[usernameAttribute]; username != nil { + mapOpts = append(mapOpts, vault.WithOverrideUsernameAttribute(username.(string))) + } + if password := overrides[passwordAttribute]; password != nil { + mapOpts = append(mapOpts, vault.WithOverridePasswordAttribute(password.(string))) + } + if len(mapOpts) > 0 { + opts = append(opts, vault.WithMappingOverride(vault.NewUserPasswordOverride(mapOpts...))) + } + } + cs, err := vault.NewCredentialLibrary(storeId, attrs.GetPath().GetValue(), opts...) if err != nil { return nil, errors.WrapDeprecated(err, op, errors.WithMsg("unable to build credential library")) @@ -549,6 +596,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()) default: badFields[globals.CredentialStoreIdField] = "This field must be a valid credential store id." } @@ -600,3 +648,30 @@ func validateListRequest(req *pbs.ListCredentialLibrariesRequest) error { } return nil } + +func validateMapping(badFields map[string]string, credentialType string, overrides map[string]interface{}) { + validFields := make(map[string]bool) + switch credential.Type(credentialType) { + case "": + if len(overrides) > 0 { + badFields[globals.CredentialMappingOverridesField] = fmt.Sprintf("This field can only be set if %q is set", globals.CredentialTypeField) + } + return + case credential.UserPasswordType: + validFields[usernameAttribute] = true + validFields[passwordAttribute] = true + default: + badFields[globals.CredentialTypeField] = fmt.Sprintf("Unknown credential type %q", credentialType) + return + } + + for k, v := range overrides { + if ok := validFields[k]; !ok { + badFields[globals.CredentialMappingOverridesField+"."+k] = fmt.Sprintf("Invalid mapping override for credential type %q", credentialType) + continue + } + if _, ok := v.(string); v != nil && !ok { + badFields[globals.CredentialMappingOverridesField+"."+k] = fmt.Sprintf("Mapping value must be a string or a null to clear, got %T", v) + } + } +} diff --git a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index 681294a1a..3e1e7e5b9 100644 --- a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" @@ -326,6 +327,47 @@ func TestCreate(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "Invalid credential type", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }) + require.NoError(t, err) + return attrs + }(), + CredentialType: "fake-type", + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid user_password mapping", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }) + require.NoError(t, err) + return attrs + }(), + CredentialType: string(credential.UserPasswordType), + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "user-test", + "invalid": "invalid-test", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, { name: "Using POST method", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ @@ -403,6 +445,151 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "Create a valid vault CredentialLibrary user_password type", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }) + require.NoError(t, err) + return attrs + }(), + CredentialType: string(credential.UserPasswordType), + }}, + idPrefix: vault.CredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.CredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.Subtype.String(), + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + HttpMethod: wrapperspb.String("GET"), + }) + require.NoError(t, err) + return attrs + }(), + CredentialType: string(credential.UserPasswordType), + AuthorizedActions: testAuthorizedActions, + }, + }, + }, + { + name: "Create a valid vault CredentialLibrary user_password type with username mapping", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }) + require.NoError(t, err) + return attrs + }(), + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "user-test", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + CredentialType: string(credential.UserPasswordType), + }}, + idPrefix: vault.CredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.CredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.Subtype.String(), + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + HttpMethod: wrapperspb.String("GET"), + }) + require.NoError(t, err) + return attrs + }(), + CredentialType: string(credential.UserPasswordType), + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "user-test", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + AuthorizedActions: testAuthorizedActions, + }, + }, + }, + { + name: "Create a valid vault CredentialLibrary user_password type with username/password mapping", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }) + require.NoError(t, err) + return attrs + }(), + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "user-test", + passwordAttribute: "pass-test", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + CredentialType: string(credential.UserPasswordType), + }}, + idPrefix: vault.CredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.CredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.Subtype.String(), + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + HttpMethod: wrapperspb.String("GET"), + }) + require.NoError(t, err) + return attrs + }(), + CredentialType: string(credential.UserPasswordType), + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "user-test", + passwordAttribute: "pass-test", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + AuthorizedActions: testAuthorizedActions, + }, + }, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go b/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go index a042456b7..d9dcac8fa 100644 --- a/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go +++ b/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go @@ -54,6 +54,10 @@ type CredentialLibrary struct { Attributes *structpb.Struct `protobuf:"bytes,100,opt,name=attributes,proto3" json:"attributes,omitempty"` // Output only. The available actions on this resource for this user. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty"` + // The type of credential this library will issue, defaults to Unspecified + CredentialType string `protobuf:"bytes,310,opt,name=credential_type,proto3" json:"credential_type,omitempty"` + // The credential mapping overrides + CredentialMappingOverrides *structpb.Struct `protobuf:"bytes,320,opt,name=credential_mapping_overrides,proto3" json:"credential_mapping_overrides,omitempty"` } func (x *CredentialLibrary) Reset() { @@ -165,6 +169,20 @@ func (x *CredentialLibrary) GetAuthorizedActions() []string { return nil } +func (x *CredentialLibrary) GetCredentialType() string { + if x != nil { + return x.CredentialType + } + return "" +} + +func (x *CredentialLibrary) GetCredentialMappingOverrides() *structpb.Struct { + if x != nil { + return x.CredentialMappingOverrides + } + return nil +} + // The attributes of a vault typed Credential Library. type VaultCredentialLibraryAttributes struct { state protoimpl.MessageState @@ -254,7 +272,7 @@ var file_controller_api_resources_credentiallibraries_v1_credential_library_prot 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe4, 0x04, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf9, 0x05, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, @@ -292,37 +310,47 @@ var file_controller_api_resources_credentiallibraries_v1_credential_library_prot 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xee, 0x02, - 0x0a, 0x20, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, - 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x12, 0x56, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x24, - 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x74, 0x68, 0x12, 0x09, 0x56, 0x61, 0x75, 0x6c, 0x74, - 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x6c, 0x0a, 0x0b, 0x68, 0x74, - 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2c, 0xa0, - 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, - 0x0a, 0x48, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0b, 0x68, 0x74, 0x74, - 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x83, 0x01, 0x0a, 0x11, 0x68, 0x74, 0x74, - 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x1e, + 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2f, 0x0a, + 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0xb6, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x0f, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x62, + 0x0a, 0x1c, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6d, 0x61, 0x70, + 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x18, 0xc0, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x04, + 0xa0, 0xda, 0x29, 0x01, 0x52, 0x1c, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x73, 0x22, 0xee, 0x02, 0x0a, 0x20, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x42, 0x24, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x0f, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x74, 0x68, 0x12, 0x09, + 0x56, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, + 0x6c, 0x0a, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x42, 0x37, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x2f, 0x0a, 0x1c, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x0f, 0x48, 0x74, 0x74, 0x70, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x52, 0x11, 0x68, 0x74, 0x74, - 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x42, 0x68, - 0x5a, 0x66, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, - 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, 0x62, 0x72, 0x61, - 0x72, 0x69, 0x65, 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, - 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x65, 0x42, 0x2c, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0a, 0x48, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x52, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x83, 0x01, + 0x0a, 0x11, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, + 0x6f, 0x64, 0x79, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x37, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, + 0x2f, 0x0a, 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x68, 0x74, + 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x12, + 0x0f, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, + 0x52, 0x11, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, + 0x6f, 0x64, 0x79, 0x42, 0x68, 0x5a, 0x66, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -347,20 +375,21 @@ var file_controller_api_resources_credentiallibraries_v1_credential_library_prot (*structpb.Struct)(nil), // 5: google.protobuf.Struct } var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_depIdxs = []int32{ - 2, // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 3, // 1: controller.api.resources.credentiallibraries.v1.CredentialLibrary.name:type_name -> google.protobuf.StringValue - 3, // 2: controller.api.resources.credentiallibraries.v1.CredentialLibrary.description:type_name -> google.protobuf.StringValue - 4, // 3: controller.api.resources.credentiallibraries.v1.CredentialLibrary.created_time:type_name -> google.protobuf.Timestamp - 4, // 4: controller.api.resources.credentiallibraries.v1.CredentialLibrary.updated_time:type_name -> google.protobuf.Timestamp - 5, // 5: controller.api.resources.credentiallibraries.v1.CredentialLibrary.attributes:type_name -> google.protobuf.Struct - 3, // 6: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue - 3, // 7: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_method:type_name -> google.protobuf.StringValue - 3, // 8: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_request_body:type_name -> google.protobuf.StringValue - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 2, // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 3, // 1: controller.api.resources.credentiallibraries.v1.CredentialLibrary.name:type_name -> google.protobuf.StringValue + 3, // 2: controller.api.resources.credentiallibraries.v1.CredentialLibrary.description:type_name -> google.protobuf.StringValue + 4, // 3: controller.api.resources.credentiallibraries.v1.CredentialLibrary.created_time:type_name -> google.protobuf.Timestamp + 4, // 4: controller.api.resources.credentiallibraries.v1.CredentialLibrary.updated_time:type_name -> google.protobuf.Timestamp + 5, // 5: controller.api.resources.credentiallibraries.v1.CredentialLibrary.attributes:type_name -> google.protobuf.Struct + 5, // 6: controller.api.resources.credentiallibraries.v1.CredentialLibrary.credential_mapping_overrides:type_name -> google.protobuf.Struct + 3, // 7: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue + 3, // 8: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_method:type_name -> google.protobuf.StringValue + 3, // 9: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_request_body:type_name -> google.protobuf.StringValue + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_controller_api_resources_credentiallibraries_v1_credential_library_proto_init() } From 17762ed01ee54dcd93db3bed615cf78054d2ad26 Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Sat, 13 Nov 2021 07:34:02 -0500 Subject: [PATCH 03/12] test(vault): Fix use of assert library in tests --- .../vault/repository_credentials_test.go | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 3ddc2b1db..5078b2418 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -36,14 +36,12 @@ func TestRepository_IssueCredentials(t *testing.T) { org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) kms := kms.TestKms(t, conn, wrapper) - assert, require := assert.New(t), require.New(t) - sche := scheduler.TestScheduler(t, conn, wrapper) repo, err := vault.NewRepository(rw, rw, kms, sche) - require.NoError(err) - require.NotNil(repo) + require.NoError(t, err) + require.NotNil(t, repo) err = vault.RegisterJobs(ctx, sche, rw, rw, kms) - require.NoError(err) + require.NoError(t, err) _, token := v.CreateToken(t, vault.WithPolicies([]string{"default", "boundary-controller", "database", "pki", "secret"})) @@ -53,15 +51,15 @@ func TestRepository_IssueCredentials(t *testing.T) { var opts []vault.Option opts = append(opts, vault.WithCACert(v.CaCert)) clientCert, err := vault.NewClientCertificate(v.ClientCert, v.ClientKey) - require.NoError(err) + require.NoError(t, err) opts = append(opts, vault.WithClientCert(clientCert)) credStoreIn, err := vault.NewCredentialStore(prj.GetPublicId(), v.Addr, []byte(token), opts...) - assert.NoError(err) - require.NotNil(credStoreIn) + assert.NoError(t, err) + require.NotNil(t, credStoreIn) origStore, err := repo.CreateCredentialStore(ctx, credStoreIn) - assert.NoError(err) - require.NotNil(origStore) + assert.NoError(t, err) + require.NotNil(t, origStore) type libT int const ( @@ -76,32 +74,32 @@ func TestRepository_IssueCredentials(t *testing.T) { { libPath := path.Join("database", "creds", "opened") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath) - assert.NoError(err) - require.NotNil(libIn) + assert.NoError(t, err) + require.NotNil(t, libIn) lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) - assert.NoError(err) - require.NotNil(lib) + assert.NoError(t, err) + require.NotNil(t, lib) libs[libDB] = lib.GetPublicId() } { libPath := path.Join("pki", "issue", "boundary") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, vault.WithMethod(vault.MethodPost), vault.WithRequestBody([]byte(`{"common_name":"boundary.com"}`))) - assert.NoError(err) - require.NotNil(libIn) + assert.NoError(t, err) + require.NotNil(t, libIn) lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) - assert.NoError(err) - require.NotNil(lib) + assert.NoError(t, err) + require.NotNil(t, lib) libs[libPKI] = lib.GetPublicId() } { libPath := path.Join("pki", "issue", "boundary") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, vault.WithMethod(vault.MethodPost)) - assert.NoError(err) - require.NotNil(libIn) + assert.NoError(t, err) + require.NotNil(t, libIn) lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) - assert.NoError(err) - require.NotNil(lib) + assert.NoError(t, err) + require.NotNil(t, lib) libs[libErrPKI] = lib.GetPublicId() } { @@ -229,6 +227,7 @@ func TestRepository_IssueCredentials(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), From 0e637f47b542851c7bfc908b1e160a0201d39131 Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Wed, 10 Nov 2021 06:47:21 -0500 Subject: [PATCH 04/12] feat(credential): Use credential type when issuing credentials This change uses the credential type attribute when issuing credentials including mapping overrides. --- internal/credential/credential.go | 4 +- internal/credential/vault/private_library.go | 260 +++++++++++++----- .../credential/vault/private_library_test.go | 255 ++++++++++++++++- .../vault/repository_credentials.go | 53 +--- .../vault/repository_credentials_test.go | 87 +++++- .../22/05_vault_private_library.up.sql | 48 ++++ .../sqltest/initdb.d/03_widgets_persona.sql | 5 + ...library_user_password_mapping_override.sql | 19 +- internal/errors/code.go | 1 + internal/errors/code_test.go | 5 + internal/errors/info.go | 4 + 11 files changed, 617 insertions(+), 124 deletions(-) create mode 100644 internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 1029448fe..7c1cc07b3 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -32,9 +32,7 @@ const ( type Library interface { boundary.Resource GetStoreId() string - - // TODO(mgaffney) 10/2021: Add method for returning the credential type - // of the library + CredentialType() Type } // Purpose is the purpose of the credential. diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 12a27f203..393616551 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "strings" + "time" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db/timestamp" @@ -12,84 +13,153 @@ import ( "github.com/hashicorp/boundary/internal/kms" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" + vault "github.com/hashicorp/vault/api" "google.golang.org/protobuf/proto" ) -var _ credential.Dynamic = (*actualCredential)(nil) +var _ credential.UserPassword = (*usrPassCred)(nil) -type actualCredential struct { - id string - sessionId string - lib *privateLibrary - secretData map[string]interface{} - purpose credential.Purpose +type usrPassCred struct { + *baseCred + username string + password credential.Password } -func (ac *actualCredential) GetPublicId() string { return ac.id } -func (ac *actualCredential) GetSessionId() string { return ac.sessionId } -func (ac *actualCredential) Secret() credential.SecretData { return ac.secretData } -func (ac *actualCredential) Library() credential.Library { return ac.lib } -func (ac *actualCredential) Purpose() credential.Purpose { return ac.purpose } +func (c *usrPassCred) Username() string { return c.username } +func (c *usrPassCred) Password() credential.Password { return c.password } + +var _ credential.Dynamic = (*baseCred)(nil) + +type baseCred struct { + *Credential + + lib *privateLibrary + secretData map[string]interface{} +} + +func (bc *baseCred) Secret() credential.SecretData { return bc.secretData } +func (bc *baseCred) Library() credential.Library { return bc.lib } +func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.Purpose } +func (bc *baseCred) getExpiration() time.Duration { return bc.expiration } + +// convert converts bc to a specific credential type if bc is not +// UnspecifiedType. +func convert(ctx context.Context, bc *baseCred) (dynamicCred, error) { + switch bc.Library().CredentialType() { + case credential.UserPasswordType: + return baseToUsrPass(ctx, bc) + } + return bc, nil +} + +func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) { + switch { + case bc == nil: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred")) + case bc.lib == nil: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred.lib")) + case bc.Library().CredentialType() != credential.UserPasswordType: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type")) + } + + uAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PasswordAttribute + if uAttr == "" { + uAttr = "username" + } + if pAttr == "" { + pAttr = "password" + } + + var username, password string + if u, ok := bc.secretData[uAttr]; ok { + if u, ok := u.(string); ok { + username = u + } + } + if p, ok := bc.secretData[pAttr]; ok { + if p, ok := p.(string); ok { + password = p + } + } + + if username == "" || password == "" { + return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping)) + } + + return &usrPassCred{ + baseCred: bc, + username: username, + password: credential.Password(password), + }, nil +} var _ credential.Library = (*privateLibrary)(nil) +// A privateLibrary contains all the values needed to connect to Vault and +// retrieve credentials. type privateLibrary struct { - PublicId string `gorm:"primary_key"` - StoreId string - Name string - Description string - CreateTime *timestamp.Timestamp - UpdateTime *timestamp.Timestamp - Version uint32 - ScopeId string - VaultPath string - HttpMethod string - HttpRequestBody []byte - VaultAddress string - Namespace string - CaCert []byte - TlsServerName string - TlsSkipVerify bool - TokenHmac []byte - Token TokenSecret - CtToken []byte - TokenKeyId string - ClientCert []byte - ClientKey KeySecret - CtClientKey []byte - ClientKeyId string - Purpose credential.Purpose `gorm:"-"` + PublicId string `gorm:"primary_key"` + StoreId string + CredType string `gorm:"column:credential_type"` + UsernameAttribute string + PasswordAttribute string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + ScopeId string + VaultPath string + HttpMethod string + HttpRequestBody []byte + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + TokenHmac []byte + Token TokenSecret + CtToken []byte + TokenKeyId string + ClientCert []byte + ClientKey KeySecret + CtClientKey []byte + ClientKeyId string + Purpose credential.Purpose `gorm:"-"` } func (pl *privateLibrary) clone() *privateLibrary { // The 'append(a[:0:0], a...)' comes from // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F return &privateLibrary{ - PublicId: pl.PublicId, - StoreId: pl.StoreId, - Name: pl.Name, - Description: pl.Description, - CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), - UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), - Version: pl.Version, - ScopeId: pl.ScopeId, - VaultPath: pl.VaultPath, - HttpMethod: pl.HttpMethod, - HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...), - VaultAddress: pl.VaultAddress, - Namespace: pl.Namespace, - CaCert: append(pl.CaCert[:0:0], pl.CaCert...), - TlsServerName: pl.TlsServerName, - TlsSkipVerify: pl.TlsSkipVerify, - TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...), - Token: append(pl.Token[:0:0], pl.Token...), - CtToken: append(pl.CtToken[:0:0], pl.CtToken...), - TokenKeyId: pl.TokenKeyId, - ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...), - ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...), - CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), - ClientKeyId: pl.ClientKeyId, - Purpose: pl.Purpose, + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), + UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), + Version: pl.Version, + ScopeId: pl.ScopeId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...), + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: append(pl.CaCert[:0:0], pl.CaCert...), + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...), + Token: append(pl.Token[:0:0], pl.Token...), + CtToken: append(pl.CtToken[:0:0], pl.CtToken...), + TokenKeyId: pl.TokenKeyId, + ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...), + ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...), + CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, } } @@ -101,6 +171,15 @@ func (pl *privateLibrary) GetVersion() uint32 { return pl.Versi func (pl *privateLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } func (pl *privateLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } +func (pl *privateLibrary) CredentialType() credential.Type { + switch ct := pl.CredType; ct { + case "": + return credential.UnspecifiedType + default: + return credential.Type(ct) + } +} + func (pl *privateLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { const op = "vault.(privateLibrary).decrypt" @@ -157,6 +236,63 @@ func (pl *privateLibrary) client() (*client, error) { return client, nil } +type dynamicCred interface { + credential.Dynamic + getExpiration() time.Duration + insertQuery() (query string, queryValues []interface{}) + updateSessionQuery(purpose credential.Purpose) (query string, queryValues []interface{}) +} + +// retrieveCredential retrieves a dynamic credential from Vault for the +// given sessionId. +func (pl *privateLibrary) retrieveCredential(ctx context.Context, op errors.Op, sessionId string) (dynamicCred, error) { + // Get the credential ID early. No need to get a secret from Vault + // if there is no way to save it in the database. + credId, err := newCredentialId() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + client, err := pl.client() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var secret *vault.Secret + switch Method(pl.HttpMethod) { + case MethodGet: + secret, err = client.get(pl.VaultPath) + case MethodPost: + secret, err = client.post(pl.VaultPath, pl.HttpRequestBody) + default: + return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unknown http method: library: %s", pl.PublicId)) + } + + if err != nil { + // TODO(mgaffney) 05/2021: detect if the error is because of an + // expired or invalid token + return nil, errors.Wrap(ctx, err, op) + } + if secret == nil { + return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op)) + } + + leaseDuration := time.Duration(secret.LeaseDuration) * time.Second + cred, err := newCredential(pl.GetPublicId(), sessionId, secret.LeaseID, pl.TokenHmac, leaseDuration) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cred.PublicId = credId + cred.IsRenewable = secret.Renewable + + dCred := &baseCred{ + Credential: cred, + lib: pl, + secretData: secret.Data, + } + return convert(ctx, dCred) +} + // TableName returns the table name for gorm. func (pl *privateLibrary) TableName() string { return "credential_vault_library_private" diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index b2f925b08..fcc6f652d 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/scheduler" @@ -77,7 +78,7 @@ func TestRepository_getPrivateLibraries(t *testing.T) { assert.NotNil(origLookup.Token()) assert.Equal(origStore.GetPublicId(), origLookup.GetPublicId()) - libs := make(map[string]*CredentialLibrary, 3) + libs := make(map[string]*CredentialLibrary) var requests []credential.Request { libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path") @@ -112,6 +113,72 @@ func TestRepository_getPrivateLibraries(t *testing.T) { req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} requests = append(requests, req) } + { + opts := []Option{ + WithCredentialType(credential.UserPasswordType), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.UserPasswordType), + WithMappingOverride(NewUserPasswordOverride( + WithOverrideUsernameAttribute("test-username"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.UserPasswordType), + WithMappingOverride(NewUserPasswordOverride( + WithOverridePasswordAttribute("test-password"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.UserPasswordType), + WithMappingOverride(NewUserPasswordOverride( + WithOverrideUsernameAttribute("test-username"), + WithOverridePasswordAttribute("test-password"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } gotLibs, err := repo.getPrivateLibraries(ctx, requests) assert.NoError(err) @@ -131,6 +198,16 @@ func TestRepository_getPrivateLibraries(t *testing.T) { assert.Equal(want.VaultPath, got.VaultPath) assert.Equal(want.HttpMethod, got.HttpMethod) assert.Equal(want.HttpRequestBody, got.HttpRequestBody) + assert.Equal(want.CredentialType(), got.CredentialType()) + if mo := want.MappingOverride; mo != nil { + switch w := mo.(type) { + case *UserPasswordOverride: + assert.Equal(w.UsernameAttribute, got.UsernameAttribute) + assert.Equal(w.PasswordAttribute, got.PasswordAttribute) + default: + assert.Fail("unknown mapping override type") + } + } } }) } @@ -227,3 +304,179 @@ func TestRequestMap(t *testing.T) { }) } } + +func TestBaseToUsrPass(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + given *baseCred + want *usrPassCred + wantErr errors.Code + }{ + { + name: "nil-input", + wantErr: errors.InvalidParameter, + }, + { + name: "nil-library", + given: &baseCred{}, + wantErr: errors.InvalidParameter, + }, + { + name: "library-not-username-password-type", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UnspecifiedType), + }, + }, + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-no-username-default-password-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "password": "my-password", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-no-password-default-username-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "username": "my-username", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "valid-default-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "username": "my-username", + "password": "my-password", + }, + }, + want: &usrPassCred{ + username: "my-username", + password: credential.Password("my-password"), + }, + }, + { + name: "valid-override-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "test-username", + PasswordAttribute: "test-password", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + want: &usrPassCred{ + username: "override-username", + password: credential.Password("override-password"), + }, + }, + { + name: "valid-default-username-override-password", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + PasswordAttribute: "test-password", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + want: &usrPassCred{ + username: "default-username", + password: credential.Password("override-password"), + }, + }, + { + name: "valid-override-username-default-password", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "test-username", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + want: &usrPassCred{ + username: "override-username", + password: credential.Password("default-password"), + }, + }, + { + name: "invalid-username-override", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "missing-username", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-password-override", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "missing-password", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := baseToUsrPass(context.Background(), tt.given) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + want := tt.want + want.baseCred = tt.given + assert.Equal(want, got) + }) + } +} diff --git a/internal/credential/vault/repository_credentials.go b/internal/credential/vault/repository_credentials.go index af19a2e72..575d1d662 100644 --- a/internal/credential/vault/repository_credentials.go +++ b/internal/credential/vault/repository_credentials.go @@ -2,13 +2,11 @@ package vault import ( "context" - "fmt" "time" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" - vault "github.com/hashicorp/vault/api" ) var _ credential.Issuer = (*Repository)(nil) @@ -36,48 +34,13 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre var creds []credential.Dynamic var minLease time.Duration for _, lib := range libs { - // Get the credential ID early. No need to get a secret from Vault - // if there is no way to save it in the database. - credId, err := newCredentialId() + cred, err := lib.retrieveCredential(ctx, op, sessionId) if err != nil { - return nil, errors.Wrap(ctx, err, op) + return nil, err } - - client, err := lib.client() - if err != nil { - return nil, errors.Wrap(ctx, err, op) + if minLease > cred.getExpiration() { + minLease = cred.getExpiration() } - - var secret *vault.Secret - switch Method(lib.HttpMethod) { - case MethodGet: - secret, err = client.get(lib.VaultPath) - case MethodPost: - secret, err = client.post(lib.VaultPath, lib.HttpRequestBody) - default: - return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unknown http method: library: %s", lib.PublicId)) - } - - if err != nil { - // TODO(mgaffney) 05/2021: detect if the error is because of an - // expired or invalid token - return nil, errors.Wrap(ctx, err, op) - } - if secret == nil { - return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op)) - } - - leaseDuration := time.Duration(secret.LeaseDuration) * time.Second - if minLease > leaseDuration { - minLease = leaseDuration - } - cred, err := newCredential(lib.GetPublicId(), sessionId, secret.LeaseID, lib.TokenHmac, leaseDuration) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - cred.PublicId = credId - cred.IsRenewable = secret.Renewable - insertQuery, insertQueryValues := cred.insertQuery() updateQuery, updateQueryValues := cred.updateSessionQuery(lib.Purpose) if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, @@ -105,13 +68,7 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre return nil, errors.Wrap(ctx, err, op) } - creds = append(creds, &actualCredential{ - id: cred.PublicId, - sessionId: cred.SessionId, - lib: lib, - secretData: secret.Data, - purpose: lib.Purpose, - }) + creds = append(creds, cred) } // Best effort update next run time of credential renewal job, but an error should not diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 5078b2418..7689f661a 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -64,6 +64,8 @@ func TestRepository_IssueCredentials(t *testing.T) { type libT int const ( libDB libT = iota + libUsrPassDB + libErrUsrPassDB libPKI libErrPKI libKV @@ -105,23 +107,53 @@ func TestRepository_IssueCredentials(t *testing.T) { { libPath := path.Join("secret", "data", "my-secret") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) - assert.NoError(err) - require.NotNil(libIn) + assert.NoError(t, err) + require.NotNil(t, libIn) lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) - assert.NoError(err) - require.NotNil(lib) + assert.NoError(t, err) + require.NotNil(t, lib) libs[libKV] = lib.GetPublicId() } { libPath := path.Join("secret", "data", "fake-secret") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) - assert.NoError(err) - require.NotNil(libIn) + assert.NoError(t, err) + require.NotNil(t, libIn) lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) - assert.NoError(err) - require.NotNil(lib) + assert.NoError(t, err) + require.NotNil(t, lib) libs[libErrKV] = lib.GetPublicId() } + { + libPath := path.Join("database", "creds", "opened") + opts := []vault.Option{ + vault.WithCredentialType(credential.UserPasswordType), + } + libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) + assert.NoError(t, err) + require.NotNil(t, libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(t, err) + require.NotNil(t, lib) + libs[libUsrPassDB] = lib.GetPublicId() + } + { + libPath := path.Join("database", "creds", "opened") + opts := []vault.Option{ + vault.WithCredentialType(credential.UserPasswordType), + vault.WithMappingOverride(vault.NewUserPasswordOverride( + vault.WithOverrideUsernameAttribute("test-username"), + vault.WithOverridePasswordAttribute("test-password"), + )), + } + libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) + assert.NoError(t, err) + require.NotNil(t, libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(t, err) + require.NotNil(t, lib) + libs[libErrUsrPassDB] = lib.GetPublicId() + } at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) uId := at.GetIamUserId() @@ -212,6 +244,16 @@ func TestRepository_IssueCredentials(t *testing.T) { }, }, }, + { + name: "one-valid-username-password-library", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libUsrPassDB], + Purpose: credential.ApplicationPurpose, + }, + }, + }, { name: "invalid-kv-does-not-exist", convertFn: rc2dc, @@ -223,6 +265,17 @@ func TestRepository_IssueCredentials(t *testing.T) { }, wantErr: errors.VaultEmptySecret, }, + { + name: "one-valid-username-password-library", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libErrUsrPassDB], + Purpose: credential.ApplicationPurpose, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, } for _, tt := range tests { tt := tt @@ -245,7 +298,23 @@ func TestRepository_IssueCredentials(t *testing.T) { return } assert.Len(got, len(tt.requests)) - assert.NoError(err) + require.NoError(err) + assert.NotZero(len(got)) + for _, dc := range got { + switch dc.Library().CredentialType() { + case credential.UserPasswordType: + if upc, ok := dc.(credential.UserPassword); ok { + assert.NotEmpty(upc.Username()) + assert.NotEmpty(upc.Password()) + break + } + assert.Fail("want UserPassword credential from library with credential type UserPassword") + case credential.UnspecifiedType: + if _, ok := dc.(credential.UserPassword); ok { + assert.Fail("do not want UserPassword credential from library with credential type Unspecified") + } + } + } }) } } diff --git a/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql b/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql new file mode 100644 index 000000000..ac9fa30c2 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql @@ -0,0 +1,48 @@ +begin; + + drop view credential_vault_library_private; + + create view credential_vault_library_private as + with + password_override (library_id, username_attribute, password_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_user_password_mapping_override + ) + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + library.http_method as http_method, + library.http_request_body as http_request_body, + library.credential_type as credential_type, + store.scope_id as scope_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.token_hmac as token_hmac, + store.ct_token as ct_token, -- encrypted + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + upasso.username_attribute as username_attribute, + upasso.password_attribute as password_attribute + from credential_vault_library library + join credential_vault_store_private store + on library.store_id = store.public_id + left join password_override upasso + on library.public_id = upasso.library_id + and store.token_status = 'current'; + comment on view credential_vault_library_private is + 'credential_vault_library_private is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' + 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + +commit; diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 50e092b28..21603875c 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -313,6 +313,11 @@ begin; values ('p____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default'); + insert into credential_vault_token + (store_id, key_id, status, token_hmac, token, last_renewal_time, expiration_time) + values + ('vs_______wvs', 'kdkv___widget', 'current', 'hmac-value', 'token-value', now(), now() + interval '1 hour'); + insert into credential_vault_library (store_id, public_id, name, description, vault_path, http_method, credential_type) values diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql index a0e7186b3..139992c87 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql @@ -4,7 +4,7 @@ -- delete_credential_vault_library_mapping_override_subtype begin; - select plan(10); + select plan(11); select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); -- validate the setup data @@ -16,6 +16,23 @@ begin; from credential_vault_library_mapping_override where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); + prepare select_private_libraries as + select public_id::text, credential_type::text, username_attribute::text, password_attribute::text + from credential_vault_library_private + where public_id in ('vl______wvl2', 'vl______wvl3', 'vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7') + order by public_id; + + select results_eq( + 'select_private_libraries', + $$VALUES + ('vl______wvl2', 'unspecified', null, null), + ('vl______wvl3', 'user_password', null, null), + ('vl______wvl4', 'user_password', null, null), + ('vl______wvl5', 'user_password', 'my_username', null), + ('vl______wvl6', 'user_password', null, 'my_password'), + ('vl______wvl7', 'user_password', 'my_username', 'my_password')$$ + ); + -- validate the insert triggers select is(count(*), 0::bigint) from credential_vault_library_user_password_mapping_override where library_id = 'vl______wvl3'; select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3'; diff --git a/internal/errors/code.go b/internal/errors/code.go index f343de31b..8ff7681bb 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -109,6 +109,7 @@ const ( VaultCredentialRequest Code = 3014 // VaultCredentialRequest represents an error returned from Vault when retrieving a credential VaultEmptySecret Code = 3015 // VaultEmptySecret represents a empty secret was returned from Vault without error VaultInvalidMappingOverride Code = 3016 // VaultInvalidMappingOverride represents an error returned when a credential mapping is unknown or does not match a credential type + VaultInvalidCredentialMapping Code = 3017 // VaultInvalidCredentialMapping represents an error returned when a Vault secret failed to be mapped to a specific credential type // OIDC authentication provided errors OidcProviderCallbackError Code = 4000 // OidcProviderCallbackError represents an error that is passed by the OIDC provider to the callback endpoint diff --git a/internal/errors/code_test.go b/internal/errors/code_test.go index 7f80a2aba..8bc005f79 100644 --- a/internal/errors/code_test.go +++ b/internal/errors/code_test.go @@ -292,6 +292,11 @@ func TestCode_Both_String_Info(t *testing.T) { c: VaultInvalidMappingOverride, want: VaultInvalidMappingOverride, }, + { + name: "VaultInvalidCredentialMapping", + c: VaultInvalidCredentialMapping, + want: VaultInvalidCredentialMapping, + }, { name: "OidcProviderCallbackError", c: OidcProviderCallbackError, diff --git a/internal/errors/info.go b/internal/errors/info.go index 31e6aafda..9038edc54 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -236,6 +236,10 @@ var errorCodeInfo = map[Code]Info{ Message: "invalid credential mapping override", Kind: Parameter, }, + VaultInvalidCredentialMapping: { + Message: "mapping vault secret to a credential type failed", + Kind: Integrity, + }, OidcProviderCallbackError: { Message: "oidc provider callback error", Kind: External, From ba8188e6d7a4ab1fd309c5599c827710019d4ba0 Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Fri, 3 Dec 2021 14:50:40 -0500 Subject: [PATCH 05/12] feat(credential): Add mapping override to credential library on lookup This change returns any mapping override set on a credential library when the library is retrieved from the repository using the lookup method. --- .../vault/repository_credential_library.go | 59 +++- .../repository_credential_library_test.go | 255 ++++++++++++++---- .../postgres/10/04_vault_credential.up.sql | 1 + .../22/05_vault_private_library.up.sql | 20 ++ 4 files changed, 277 insertions(+), 58 deletions(-) diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index 6d2c763cd..2c1d8e0a7 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/boundary/internal/db" dbcommon "github.com/hashicorp/boundary/internal/db/common" + "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" @@ -214,7 +215,7 @@ func (r *Repository) LookupCredentialLibrary(ctx context.Context, publicId strin if publicId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") } - l := allocCredentialLibrary() + l := allocPublicLibrary() l.PublicId = publicId if err := r.reader.LookupByPublicId(ctx, l); err != nil { if errors.IsNotFoundError(err) { @@ -222,9 +223,63 @@ func (r *Repository) LookupCredentialLibrary(ctx context.Context, publicId strin } return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", publicId))) } - return l, nil + return l.toCredentialLibrary(), nil } +// publicLibrary is a credential library and any of library's credential +// mapping overrides. It does not include encrypted data and is safe to +// return external to boundary. +type publicLibrary struct { + PublicId string `gorm:"primary_key"` + StoreId string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + VaultPath string + HttpMethod string + HttpRequestBody []byte + CredentialType string + UsernameAttribute string + PasswordAttribute string +} + +func allocPublicLibrary() *publicLibrary { + return &publicLibrary{} +} + +func (pl *publicLibrary) toCredentialLibrary() *CredentialLibrary { + cl := allocCredentialLibrary() + cl.PublicId = pl.PublicId + cl.StoreId = pl.StoreId + cl.Name = pl.Name + cl.Description = pl.Description + cl.CreateTime = pl.CreateTime + cl.UpdateTime = pl.UpdateTime + cl.Version = pl.Version + cl.VaultPath = pl.VaultPath + cl.HttpMethod = pl.HttpMethod + cl.HttpRequestBody = pl.HttpRequestBody + cl.CredentialLibrary.CredentialType = pl.CredentialType + + if pl.UsernameAttribute != "" || pl.PasswordAttribute != "" { + up := allocUserPasswordOverride() + up.LibraryId = pl.PublicId + up.UsernameAttribute = pl.UsernameAttribute + up.PasswordAttribute = pl.PasswordAttribute + up.sanitize() + cl.MappingOverride = up + } + return cl +} + +// TableName returns the table name for gorm. +func (_ *publicLibrary) TableName() string { return "credential_vault_library_public" } + +// GetPublicId returns the public id. +func (pl *publicLibrary) GetPublicId() string { return pl.PublicId } + // DeleteCredentialLibrary deletes publicId from the repository and returns // the number of records deleted. func (r *Repository) DeleteCredentialLibrary(ctx context.Context, scopeId string, publicId string, _ ...Option) (int, error) { diff --git a/internal/credential/vault/repository_credential_library_test.go b/internal/credential/vault/repository_credential_library_test.go index a3caaf27d..2b3b876f9 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -212,9 +212,11 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { wantErr: errors.VaultInvalidMappingOverride, }, { - name: "valid-user-password-credential-type-with-override", + name: "valid-user-password-credential-type-with-username-override", in: &CredentialLibrary{ - MappingOverride: NewUserPasswordOverride(WithOverrideUsernameAttribute("test")), + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("utest"), + ), CredentialLibrary: &store.CredentialLibrary{ StoreId: cs.GetPublicId(), HttpMethod: "GET", @@ -223,7 +225,61 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { }, }, want: &CredentialLibrary{ - MappingOverride: NewUserPasswordOverride(WithOverrideUsernameAttribute("test")), + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("utest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, + { + name: "valid-user-password-credential-type-with-password-override", + in: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverridePasswordAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverridePasswordAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, + { + name: "valid-user-password-credential-type-with-username-and-password-override", + in: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("utest"), + WithOverridePasswordAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("utest"), + WithOverridePasswordAttribute("ptest"), + ), CredentialLibrary: &store.CredentialLibrary{ StoreId: cs.GetPublicId(), HttpMethod: "GET", @@ -1021,64 +1077,151 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) - _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] - l := TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 1)[0] + { + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] - badId, err := newCredentialLibraryId() - require.NoError(t, err) - require.NotNil(t, badId) + tests := []struct { + name string + in *CredentialLibrary + }{ + { + name: "valid-no-options", + in: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + }, + }, + }, + { + name: "valid-user-password-credential-type", + in: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, + { + name: "valid-user-password-credential-type-with-username-override", + in: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("utest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, + { + name: "valid-user-password-credential-type-with-password-override", + in: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverridePasswordAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, + { + name: "valid-user-password-credential-type-with-username-and-password-override", + in: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("utest"), + WithOverridePasswordAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, + } - tests := []struct { - name string - in string - want *CredentialLibrary - wantErr errors.Code - }{ - { - name: "valid", - in: l.GetPublicId(), - want: l, - }, - { - name: "empty-public-id", - in: "", - wantErr: errors.InvalidParameter, - }, - { - name: "not-found", - in: badId, - }, + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + orig, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), tt.in) + assert.NoError(err) + require.NotEmpty(orig) + // test + got, err := repo.LookupCredentialLibrary(ctx, orig.GetPublicId()) + assert.NoError(err) + require.NotEmpty(got) + assert.Equal(orig.Name, got.Name) + assert.Equal(orig.Description, got.Description) + assert.Equal(orig.CredentialType(), got.CredentialType()) + if tt.in.MappingOverride != nil { + require.NotNil(got.MappingOverride) + assert.IsType(orig.MappingOverride, got.MappingOverride) + switch w := orig.MappingOverride.(type) { + case *UserPasswordOverride: + g, ok := got.MappingOverride.(*UserPasswordOverride) + require.True(ok) + assert.Equal(w.UsernameAttribute, g.UsernameAttribute) + assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + default: + assert.Fail("Unknown mapping override") + } + } + }) + } } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - ctx := context.Background() - kms := kms.TestKms(t, conn, wrapper) - sche := scheduler.TestScheduler(t, conn, wrapper) - repo, err := NewRepository(rw, rw, kms, sche) - assert.NoError(err) - require.NotNil(repo) + t.Run("empty-public-id", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + // test + got, err := repo.LookupCredentialLibrary(ctx, "") + wantErr := errors.InvalidParameter + assert.Truef(errors.Match(errors.T(wantErr), err), "want err: %q got: %q", wantErr, err) + assert.Nil(got) + }) - got, err := repo.LookupCredentialLibrary(ctx, tt.in) - if tt.wantErr != 0 { - assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) - assert.Nil(got) - return - } - require.NoError(err) - - switch { - case tt.want == nil: - assert.Nil(got) - case tt.want != nil: - assert.NotNil(got) - assert.Equal(got, tt.want) - } - }) - } + t.Run("not-found", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + badId, err := newCredentialLibraryId() + assert.NoError(err) + require.NotNil(badId) + // test + got, err := repo.LookupCredentialLibrary(ctx, badId) + assert.NoError(err) + assert.Empty(got) + }) } func TestRepository_DeleteCredentialLibrary(t *testing.T) { diff --git a/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql b/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql index f1ecbb2a6..e47ece684 100644 --- a/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql @@ -577,6 +577,7 @@ begin; 'credential_vault_store_public is a view where each row contains a credential store. ' 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; +-- Replaced in 21/05_vault_private_library.up.sql create view credential_vault_library_private as select library.public_id as public_id, library.store_id as store_id, diff --git a/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql b/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql index ac9fa30c2..b0964bbf8 100644 --- a/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql +++ b/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql @@ -1,5 +1,6 @@ begin; +-- Replaces view from 10/04_vault_credential.up.sql drop view credential_vault_library_private; create view credential_vault_library_private as @@ -45,4 +46,23 @@ begin; 'credential_vault_library_private is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + create view credential_vault_library_public as + select public_id, + store_id, + name, + description, + create_time, + update_time, + version, + vault_path, + http_method, + http_request_body, + credential_type, + username_attribute, + password_attribute + from credential_vault_library_private; + comment on view credential_vault_library_public is + 'credential_vault_library_public is a view where each row contains a credential library and any of library''s credential mapping overrides. ' + 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; + commit; From 9cdfc5f9863a00d7d5e1344824990ba19623ace8 Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Sat, 4 Dec 2021 12:23:54 -0500 Subject: [PATCH 06/12] docs(vault): Add comment about the default value of HttpMethod --- internal/credential/vault/repository_credential_library.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index 2c1d8e0a7..faff3f278 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -166,6 +166,12 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string ) if strutil.StrListContains(nullFields, httpMethodField) { + // GET is the default value for the HttpMethod field in + // CredentialLibrary. The http_method column in the database does + // not allow NULL values but it also does not define a default + // value. Therefore, if the httpMethodField is in nullFields: + // remove it from nullFields then add it to dbMask and set the + // value to GET. dbMask = append(dbMask, httpMethodField) nullFields = strutil.StrListDelete(nullFields, httpMethodField) l.HttpMethod = string(MethodGet) From 035182681af04fc806ffbc28c63ee7aa85e0a15c Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Sat, 4 Dec 2021 15:30:42 -0500 Subject: [PATCH 07/12] feat(credential): Allow updating of credential mapping overrides --- internal/credential/vault/fields.go | 2 + .../vault/repository_credential_library.go | 130 ++++++++- .../repository_credential_library_test.go | 246 +++++++++++++++++- 3 files changed, 357 insertions(+), 21 deletions(-) diff --git a/internal/credential/vault/fields.go b/internal/credential/vault/fields.go index bd2415dd8..cad36b46b 100644 --- a/internal/credential/vault/fields.go +++ b/internal/credential/vault/fields.go @@ -17,4 +17,6 @@ const ( tlsServerNameField = "TlsServerName" tlsSkipVerifyField = "TlsSkipVerify" tokenField = "Token" + + mappingOverrideField = "MappingOverride" ) diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index faff3f278..1ded5ea07 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -114,8 +114,8 @@ func (r *Repository) CreateCredentialLibrary(ctx context.Context, scopeId string // number of records updated. l is not changed. // // l must contain a valid PublicId. Only Name, Description, VaultPath, -// HttpMethod, and HttpRequestBody can be updated. If l.Name is set to a -// non-empty string, it must be unique within l.StoreId. +// HttpMethod, HttpRequestBody, and MappingOverride can be updated. If +// l.Name is set to a non-empty string, it must be unique within l.StoreId. // // An attribute of l will be set to NULL in the database if the attribute // in l is the zero value and it is included in fieldMaskPaths except for @@ -141,6 +141,7 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string } l = l.clone() + var updateMappingOverride bool for _, f := range fieldMaskPaths { switch { case strings.EqualFold(nameField, f): @@ -148,6 +149,8 @@ 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): + updateMappingOverride = true default: return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) } @@ -160,6 +163,7 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string vaultPathField: l.VaultPath, httpMethodField: l.HttpMethod, httpRequestBodyField: l.HttpRequestBody, + mappingOverrideField: l.MappingOverride, }, fieldMaskPaths, nil, @@ -181,6 +185,32 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask") } + origLib, err := r.LookupCredentialLibrary(ctx, l.PublicId) + switch { + case err != nil: + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) + case origLib == nil: + return nil, db.NoRowsAffected, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("credential library %s", l.PublicId)) + case updateMappingOverride && !validMappingOverride(l.MappingOverride, origLib.CredentialType()): + return nil, db.NoRowsAffected, errors.New(ctx, errors.VaultInvalidMappingOverride, op, "invalid mapping override for credential type") + } + + var filteredDbMask, filteredNullFields []string + for _, f := range dbMask { + switch { + case strings.EqualFold(mappingOverrideField, f): + default: + filteredDbMask = append(filteredDbMask, f) + } + } + for _, f := range nullFields { + switch { + case strings.EqualFold(mappingOverrideField, f): + default: + filteredNullFields = append(filteredNullFields, f) + } + } + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), @@ -190,16 +220,94 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string var rowsUpdated int var returnedCredentialLibrary *CredentialLibrary _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, - func(_ db.Reader, w db.Writer) error { - returnedCredentialLibrary = l.clone() - var err error - rowsUpdated, err = w.Update(ctx, returnedCredentialLibrary, dbMask, nullFields, - db.WithOplog(oplogWrapper, l.oplog(oplog.OpType_OP_TYPE_UPDATE)), - db.WithVersion(&version)) - if err == nil && rowsUpdated > 1 { - return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated") + func(rr db.Reader, w db.Writer) error { + var msgs []*oplog.Message + ticket, err := w.GetTicket(l) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - return err + + l := l.clone() + var lOplogMsg oplog.Message + + // Update the credential library table + switch { + case len(filteredDbMask) == 0 && len(filteredNullFields) == 0: + // the credential library's fields are not being updated, + // just one of it's child objects, so we just need to + // update the library's version. + l.Version = version + 1 + rowsUpdated, err = w.Update(ctx, l, []string{"Version"}, nil, db.NewOplogMsg(&lOplogMsg), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library version")) + } + switch rowsUpdated { + case 1: + case 0: + return nil + default: + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential library version and %d rows updated", rowsUpdated)) + } + default: + rowsUpdated, err = w.Update(ctx, l, filteredDbMask, filteredNullFields, db.NewOplogMsg(&lOplogMsg), db.WithVersion(&version)) + if err != nil { + if errors.IsUniqueError(err) { + return errors.New(ctx, errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", l.Name, l.PublicId)) + } + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) + } + switch rowsUpdated { + case 1: + case 0: + return nil + default: + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential library and %d rows updated", rowsUpdated)) + } + } + msgs = append(msgs, &lOplogMsg) + + // Update a credential mapping override table if applicable + if updateMappingOverride { + // delete the current mapping override if it exists + if origLib.MappingOverride != nil { + var msg oplog.Message + rowsDeleted, err := w.Delete(ctx, origLib.MappingOverride, db.NewOplogMsg(&msg)) + switch { + case err != nil: + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete mapping override")) + default: + switch rowsDeleted { + case 0, 1: + default: + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("delete mapping override and %d rows deleted", rowsDeleted)) + } + } + msgs = append(msgs, &msg) + } + // insert a new mapping override if specified + if l.MappingOverride != nil { + var msg oplog.Message + l.MappingOverride.setLibraryId(l.PublicId) + if err := w.Create(ctx, l.MappingOverride, db.NewOplogMsg(&msg)); err != nil { + return errors.Wrap(ctx, err, op) + } + msgs = append(msgs, &msg) + } + } + + metadata := l.oplog(oplog.OpType_OP_TYPE_UPDATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } + + pl := allocPublicLibrary() + pl.PublicId = l.PublicId + if err := rr.LookupByPublicId(ctx, pl); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve updated credential library")) + } + returnedCredentialLibrary = pl.toCredentialLibrary() + return nil }, ) diff --git a/internal/credential/vault/repository_credential_library_test.go b/internal/credential/vault/repository_credential_library_test.go index 2b3b876f9..ed6ce3923 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -464,6 +464,20 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { } } + changeCredentialType := func(t credential.Type) func(*CredentialLibrary) *CredentialLibrary { + return func(l *CredentialLibrary) *CredentialLibrary { + l.CredentialLibrary.CredentialType = string(t) + return l + } + } + + changeMappingOverride := func(m MappingOverride) func(*CredentialLibrary) *CredentialLibrary { + return func(l *CredentialLibrary) *CredentialLibrary { + l.MappingOverride = m + return l + } + } + makeNil := func() func(*CredentialLibrary) *CredentialLibrary { return func(l *CredentialLibrary) *CredentialLibrary { return nil @@ -508,6 +522,7 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { wantCount int wantErr errors.Code }{ + { name: "nil-credential-library", orig: &CredentialLibrary{ @@ -746,17 +761,21 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { { name: "change-vault-path", orig: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride(WithOverrideUsernameAttribute("orig-username")), CredentialLibrary: &store.CredentialLibrary{ - HttpMethod: "GET", - VaultPath: "/old/path", + HttpMethod: "GET", + VaultPath: "/old/path", + CredentialType: string(credential.UserPasswordType), }, }, chgFn: changeVaultPath("/new/path"), masks: []string{vaultPathField}, want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride(WithOverrideUsernameAttribute("orig-username")), CredentialLibrary: &store.CredentialLibrary{ - HttpMethod: "GET", - VaultPath: "/new/path", + HttpMethod: "GET", + VaultPath: "/new/path", + CredentialType: string(credential.UserPasswordType), }, }, wantCount: 1, @@ -899,6 +918,196 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { }, wantCount: 1, }, + { + name: "read-only-credential-type-in-field-mask", + orig: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + chgFn: changeCredentialType(credential.UnspecifiedType), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "StoreId", "CredentialType"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "user-password-attributes-change-username-attribute", + orig: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("orig-username"), + WithOverridePasswordAttribute("orig-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + chgFn: changeMappingOverride( + NewUserPasswordOverride( + WithOverrideUsernameAttribute("changed-username"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("changed-username"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + wantCount: 1, + }, + { + name: "user-password-attributes-change-password-attribute", + orig: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("orig-username"), + WithOverridePasswordAttribute("orig-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + chgFn: changeMappingOverride( + NewUserPasswordOverride( + WithOverridePasswordAttribute("changed-password"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverridePasswordAttribute("changed-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + wantCount: 1, + }, + { + name: "user-password-attributes-change-username-and-password-attributes", + orig: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("orig-username"), + WithOverridePasswordAttribute("orig-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + chgFn: changeMappingOverride( + NewUserPasswordOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePasswordAttribute("changed-password"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePasswordAttribute("changed-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + wantCount: 1, + }, + { + name: "no-mapping-override-change-username-and-password-attributes", + orig: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + chgFn: changeMappingOverride( + NewUserPasswordOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePasswordAttribute("changed-password"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePasswordAttribute("changed-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + wantCount: 1, + }, + { + name: "user-password-attributes-delete-mapping-override", + orig: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("orig-username"), + WithOverridePasswordAttribute("orig-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + chgFn: changeMappingOverride(nil), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + }, + wantCount: 1, + }, + { + name: "set-mapping-override-on-unspecified-credential-type", + orig: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + }, + }, + chgFn: changeMappingOverride( + NewUserPasswordOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePasswordAttribute("changed-password"), + ), + ), + masks: []string{"MappingOverride"}, + wantErr: errors.VaultInvalidMappingOverride, + }, } for _, tt := range tests { @@ -940,19 +1149,36 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { underlyingDB, err := conn.SqlDB(ctx) require.NoError(err) dbassert := dbassert.New(t, underlyingDB) - if tt.want.Name == "" { + + switch tt.want.Name { + case "": dbassert.IsNull(got, "name") - return + default: + assert.Equal(tt.want.Name, got.Name) } - assert.Equal(tt.want.Name, got.Name) - if tt.want.Description == "" { + + switch tt.want.Description { + case "": dbassert.IsNull(got, "description") - return + default: + assert.Equal(tt.want.Description, got.Description) } - assert.Equal(tt.want.Description, got.Description) + if tt.wantCount > 0 { assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) } + + switch w := tt.want.MappingOverride.(type) { + case nil: + assert.Nil(got.MappingOverride) + case *UserPasswordOverride: + g, ok := got.MappingOverride.(*UserPasswordOverride) + require.True(ok) + assert.Equal(w.UsernameAttribute, g.UsernameAttribute) + assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + default: + assert.Fail("Unknown mapping override") + } }) } From da20a8b434012732d7a484c2e8fdeca845c4cb7c Mon Sep 17 00:00:00 2001 From: Louis Ruch Date: Thu, 2 Dec 2021 21:48:22 -0800 Subject: [PATCH 08/12] feat(credential): Add support for Vault KV2 --- .../vault/internal/userpassword/doc.go | 3 + .../internal/userpassword/userpassword.go | 96 +++++++++ .../userpassword/userpassword_test.go | 196 ++++++++++++++++++ internal/credential/vault/private_library.go | 15 +- .../credential/vault/private_library_test.go | 189 +++++++++++++++++ .../vault/repository_credentials_test.go | 30 ++- 6 files changed, 513 insertions(+), 16 deletions(-) create mode 100644 internal/credential/vault/internal/userpassword/doc.go create mode 100644 internal/credential/vault/internal/userpassword/userpassword.go create mode 100644 internal/credential/vault/internal/userpassword/userpassword_test.go diff --git a/internal/credential/vault/internal/userpassword/doc.go b/internal/credential/vault/internal/userpassword/doc.go new file mode 100644 index 000000000..13c2b4264 --- /dev/null +++ b/internal/credential/vault/internal/userpassword/doc.go @@ -0,0 +1,3 @@ +// Package userpassword provides access to the username and password +// stored in a Vault secret. +package userpassword diff --git a/internal/credential/vault/internal/userpassword/userpassword.go b/internal/credential/vault/internal/userpassword/userpassword.go new file mode 100644 index 000000000..3f04ab096 --- /dev/null +++ b/internal/credential/vault/internal/userpassword/userpassword.go @@ -0,0 +1,96 @@ +package userpassword + +type ( + data map[string]interface{} + + // extractFunc attempts to extract the username and password + // from sd using the provided attribute names, using a known + // Vault data response format. + extractFunc func(sd data, usernameAttr, passwordAttr string) (string, string) +) + +// Extract attempts to extract the values of the username and password +// stored within the provided data using the given attribute names. +// +// Extract does not return partial results, i.e. if one of the attributes +// were extracted but not the other ("", "") will be returned. +func Extract(d data, usernameAttr, passwordAttr string) (string, string) { + for _, f := range []extractFunc{ + defaultExtract, + kv2Extract, + } { + username, password := f(d, usernameAttr, passwordAttr) + if username != "" && password != "" { + // got valid username and password from secret + return username, password + } + } + + return "", "" +} + +// defaultExtract looks for the usernameAttr and passwordAttr in the data map +func defaultExtract(sd data, usernameAttr, passwordAttr string) (username string, password string) { + if u, ok := sd[usernameAttr]; ok { + if u, ok := u.(string); ok { + username = u + } + } + if p, ok := sd[passwordAttr]; ok { + if p, ok := p.(string); ok { + password = p + } + } + + return username, password +} + +// kv2Extract looks for the the usernameAttr and passwordAttr in the embedded +// 'data' field within the data map. +// +// Additionaly it validates the data is in the expected KV-v2 format: +// { +// "data": {}, +// "metadata: {} +// } +// If the format does not match, it returns ("", ""). See: +// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 +func kv2Extract(d data, usernameAttr, passwordAttr string) (username string, password string) { + var data, metadata map[string]interface{} + for k, v := range d { + switch k { + case "data": + var ok bool + if data, ok = v.(map[string]interface{}); !ok { + // data field should be of type map[string]interface{} in KV-v2 + return "", "" + } + case "metadata": + var ok bool + if metadata, ok = v.(map[string]interface{}); !ok { + // metadata field should be of type map[string]interface{} in KV-v2 + return "", "" + } + default: + // secretData contains a non valid KV-v2 top level field + return "", "" + } + } + if data == nil || metadata == nil { + // missing required KV-v2 field + return "", "" + } + + if u, ok := data[usernameAttr]; ok { + if u, ok := u.(string); ok { + username = u + } + } + if p, ok := data[passwordAttr]; ok { + if p, ok := p.(string); ok { + password = p + } + } + + return username, password +} diff --git a/internal/credential/vault/internal/userpassword/userpassword_test.go b/internal/credential/vault/internal/userpassword/userpassword_test.go new file mode 100644 index 000000000..3a6fcae7a --- /dev/null +++ b/internal/credential/vault/internal/userpassword/userpassword_test.go @@ -0,0 +1,196 @@ +package userpassword + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBaseToUsrPass(t *testing.T) { + t.Parallel() + + type args struct { + s data + uAttr string + pAttr string + } + type usrPass struct { + user string + pass string + } + tests := []struct { + name string + given args + want usrPass + }{ + { + name: "nil-input", + want: usrPass{user: "", pass: ""}, + }, + { + name: "no-input", + given: args{}, + want: usrPass{user: "", pass: ""}, + }, + { + name: "no-secret", + given: args{ + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "", pass: ""}, + }, + { + name: "no-match-username-secret", + given: args{ + s: data{ + "username-wrong": "user", + "password": "pass", + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "", pass: ""}, + }, + { + name: "no-match-username-secret", + given: args{ + s: data{ + "username": "user", + "password-wrong": "pass", + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "", pass: ""}, + }, + { + name: "valid-default", + given: args{ + s: data{ + "username": "user", + "password": "pass", + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "user", pass: "pass"}, + }, + { + name: "no-match-username-secret-kv2", + given: args{ + s: data{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username-wrong": "user", + "password": "pass", + }, + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "", pass: ""}, + }, + { + name: "no-match-password-secret-kv2", + given: args{ + s: data{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "user", + "password-wrong": "pass", + }, + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "", pass: ""}, + }, + { + name: "valid-kv2", + given: args{ + s: data{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "user", + "password": "pass", + }, + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "user", pass: "pass"}, + }, + { + name: "no-metadata-kv2", + given: args{ + s: data{ + "data": map[string]interface{}{ + "username": "user", + "password": "pass", + }, + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "", pass: ""}, + }, + { + name: "invalid-metadata-kv2", + given: args{ + s: data{ + "metadata": "string", + "data": map[string]interface{}{ + "username": "user", + "password": "pass", + }, + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "", pass: ""}, + }, + { + name: "invalid-field-kv2", + given: args{ + s: data{ + "invalid": map[string]interface{}{}, + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "user", + "password": "pass", + }, + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "", pass: ""}, + }, + { + name: "valid-order-default-first", + given: args{ + s: data{ + "username": "default-user", + "password": "default-pass", + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "kv2-user", + "password": "kv2-pass", + }, + }, + uAttr: "username", + pAttr: "password", + }, + want: usrPass{user: "default-user", pass: "default-pass"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + user, pass := Extract(tt.given.s, tt.given.uAttr, tt.given.pAttr) + assert.Equal(tt.want.user, user) + assert.Equal(tt.want.pass, pass) + }) + } +} diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 393616551..f0392b29e 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -8,6 +8,7 @@ import ( "time" "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault/internal/userpassword" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" @@ -69,19 +70,7 @@ func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) { if pAttr == "" { pAttr = "password" } - - var username, password string - if u, ok := bc.secretData[uAttr]; ok { - if u, ok := u.(string); ok { - username = u - } - } - if p, ok := bc.secretData[pAttr]; ok { - if p, ok := p.(string); ok { - password = p - } - } - + username, password := userpassword.Extract(bc.secretData, uAttr, pAttr) if username == "" || password == "" { return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping)) } diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index fcc6f652d..c2f71b62f 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -462,6 +462,195 @@ func TestBaseToUsrPass(t *testing.T) { }, wantErr: errors.VaultInvalidCredentialMapping, }, + { + name: "invalid-kv2-no-metadata-field", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "data": map[string]interface{}{ + "username": "my-username", + "password": "my-password", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-data-field", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-username-default-password-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "password": "my-password", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-passsword-default-username-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "my-username", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-invalid-metadata-type", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "metadata": "hello", + "data": map[string]interface{}{ + "username": "my-username", + "password": "my-password", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-invalid-metadata-type", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": "hello", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-additional-field", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "bad-field": "hello", + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "my-username", + "password": "my-password", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "valid-kv2-default-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "my-username", + "password": "my-password", + }, + }, + }, + want: &usrPassCred{ + username: "my-username", + password: credential.Password("my-password"), + }, + }, + { + name: "valid-kv2-override-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "test-username", + PasswordAttribute: "test-password", + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + }, + want: &usrPassCred{ + username: "override-username", + password: credential.Password("override-password"), + }, + }, + { + name: "valid-kv2-default-username-override-password", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + PasswordAttribute: "test-password", + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + }, + want: &usrPassCred{ + username: "default-username", + password: credential.Password("override-password"), + }, + }, + { + name: "valid-kv2-override-username-default-password", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "test-username", + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + }, + want: &usrPassCred{ + username: "override-username", + password: credential.Password("default-password"), + }, + }, } for _, tt := range tests { tt := tt diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 7689f661a..978663109 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -45,7 +45,7 @@ func TestRepository_IssueCredentials(t *testing.T) { _, token := v.CreateToken(t, vault.WithPolicies([]string{"default", "boundary-controller", "database", "pki", "secret"})) - // Create valid KV secret + // Create valid user password KV secret v.CreateKVSecret(t, "my-secret", []byte(`{"data":{"username":"user","password":"pass"}}`)) var opts []vault.Option @@ -70,6 +70,7 @@ func TestRepository_IssueCredentials(t *testing.T) { libErrPKI libKV libErrKV + libUsrPassKV ) libs := make(map[libT]string) @@ -154,6 +155,19 @@ func TestRepository_IssueCredentials(t *testing.T) { require.NotNil(t, lib) libs[libErrUsrPassDB] = lib.GetPublicId() } + { + libPath := path.Join("secret", "data", "my-secret") + opts := []vault.Option{ + vault.WithCredentialType(credential.UserPasswordType), + } + libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) + assert.NoError(t, err) + require.NotNil(t, libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(t, err) + require.NotNil(t, lib) + libs[libUsrPassKV] = lib.GetPublicId() + } at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) uId := at.GetIamUserId() @@ -245,7 +259,7 @@ func TestRepository_IssueCredentials(t *testing.T) { }, }, { - name: "one-valid-username-password-library", + name: "one-db-valid-username-password-library", convertFn: rc2dc, requests: []credential.Request{ { @@ -266,7 +280,7 @@ func TestRepository_IssueCredentials(t *testing.T) { wantErr: errors.VaultEmptySecret, }, { - name: "one-valid-username-password-library", + name: "invalid-username-password-library", convertFn: rc2dc, requests: []credential.Request{ { @@ -276,6 +290,16 @@ func TestRepository_IssueCredentials(t *testing.T) { }, wantErr: errors.VaultInvalidCredentialMapping, }, + { + name: "valid-kv-username-password-library", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libUsrPassKV], + Purpose: credential.ApplicationPurpose, + }, + }, + }, } for _, tt := range tests { tt := tt From e75f1d2f73e4d672e404a5159080d78c27fc48c5 Mon Sep 17 00:00:00 2001 From: Louis Ruch Date: Wed, 8 Dec 2021 15:35:41 -0800 Subject: [PATCH 09/12] 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 ae783aa66..6648c3cc0 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 cad36b46b..42826fdb6 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 1ded5ea07..3e3ffb2df 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 971dcfc38..dab4c0a38 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 1667e4d62..349b61295 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 3e1e7e5b9..38a890296 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,21 +674,21 @@ func TestGet(t *testing.T) { }{ { name: "success", - id: vl.GetPublicId(), + id: unspecifiedLib.GetPublicId(), res: &pbs.GetCredentialLibraryResponse{ Item: &pb.CredentialLibrary{ - Id: vl.GetPublicId(), - CredentialStoreId: vl.GetStoreId(), + 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: vl.CreateTime.GetTimestamp(), - UpdatedTime: vl.UpdateTime.GetTimestamp(), + CreatedTime: unspecifiedLib.CreateTime.GetTimestamp(), + UpdatedTime: unspecifiedLib.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(unspecifiedLib.GetVaultPath()), + HttpMethod: wrapperspb.String(unspecifiedLib.GetHttpMethod()), }) require.NoError(t, err) return attrs @@ -682,6 +696,40 @@ func TestGet(t *testing.T) { }, }, }, + { + name: "success-userpassword", + id: userPassLib.GetPublicId(), + res: &pbs.GetCredentialLibraryResponse{ + Item: &pb.CredentialLibrary{ + 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: userPassLib.CreateTime.GetTimestamp(), + UpdatedTime: userPassLib.UpdateTime.GetTimestamp(), + Version: 1, + Attributes: func() *structpb.Struct { + attrs, err := handlers.ProtoToStruct(&pb.VaultCredentialLibraryAttributes{ + 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 + }(), + }, + }, + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", vault.CredentialLibraryPrefix), @@ -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) { From 888f8cea03eaa232194f13e081d616720586d4ff Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Fri, 10 Dec 2021 06:59:48 -0500 Subject: [PATCH 10/12] test(credential): Add credential mapping override to list test --- .../repository_credential_library_test.go | 127 ++++++++++-------- 1 file changed, 74 insertions(+), 53 deletions(-) diff --git a/internal/credential/vault/repository_credential_library_test.go b/internal/credential/vault/repository_credential_library_test.go index ed6ce3923..5751c3e6d 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -5,8 +5,6 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/store" "github.com/hashicorp/boundary/internal/db" @@ -18,7 +16,6 @@ import ( "github.com/hashicorp/boundary/internal/scheduler" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/testing/protocmp" ) func TestRepository_CreateCredentialLibrary(t *testing.T) { @@ -1514,59 +1511,83 @@ func TestRepository_ListCredentialLibraries(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) - _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) - csA, csB := css[0], css[1] + t.Run("CredentialStore-with-a-library", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) - libs := TestCredentialLibraries(t, conn, wrapper, csA.GetPublicId(), 3) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) - tests := []struct { - name string - in string - opts []Option - want []*CredentialLibrary - wantErr errors.Code - }{ - { - name: "with-no-credential-store-id", - wantErr: errors.InvalidParameter, - }, - { - name: "CredentialStore-with-no-libraries", - in: csB.GetPublicId(), - want: []*CredentialLibrary{}, - }, - { - name: "CredentialStore-with-libraries", - in: csA.GetPublicId(), - want: libs, - }, - } + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2)[0] + lib := &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("orig-username"), + WithOverridePasswordAttribute("orig-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - ctx := context.Background() - kms := kms.TestKms(t, conn, wrapper) - sche := scheduler.TestScheduler(t, conn, wrapper) - repo, err := NewRepository(rw, rw, kms, sche) - assert.NoError(err) - require.NotNil(repo) - got, err := repo.ListCredentialLibraries(ctx, tt.in, tt.opts...) - if tt.wantErr != 0 { - assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) - assert.Nil(got) - return - } - require.NoError(err) - opts := []cmp.Option{ - cmpopts.SortSlices(func(x, y *CredentialLibrary) bool { return x.PublicId < y.PublicId }), - protocmp.Transform(), - } - assert.Empty(cmp.Diff(tt.want, got, opts...)) - }) - } + orig, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), lib) + assert.NoError(err) + require.NotNil(orig) + + // test + got, err := repo.ListCredentialLibraries(ctx, cs.GetPublicId()) + assert.NoError(err) + require.Len(got, 1) + got1 := got[0] + assert.Equal(orig.GetPublicId(), got1.GetPublicId()) + assert.Equal(orig.GetStoreId(), got1.GetStoreId()) + assert.Equal(orig.GetHttpMethod(), got1.GetHttpMethod()) + assert.Equal(orig.GetVaultPath(), got1.GetVaultPath()) + assert.Equal(orig.GetName(), got1.GetName()) + assert.Equal(orig.GetCredentialType(), got1.GetCredentialType()) + assert.Empty(got1.MappingOverride) + }) + + t.Run("with-no-credential-store-id", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + // test + got, err := repo.ListCredentialLibraries(ctx, "") + wantErr := errors.InvalidParameter + assert.Truef(errors.Match(errors.T(wantErr), err), "want err: %q got: %q", wantErr, err) + assert.Nil(got) + }) + + t.Run("CredentialStore-with-no-libraries", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + // test + got, err := repo.ListCredentialLibraries(ctx, cs.GetPublicId()) + assert.NoError(err) + assert.Empty(got) + }) } func TestRepository_ListCredentialLibraries_Limits(t *testing.T) { From 4636d349436c7d85cdf6a2fcb1ea53156e766f7b Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Fri, 10 Dec 2021 07:09:09 -0500 Subject: [PATCH 11/12] test(credential): Add credential mapping override to delete test --- .../repository_credential_library_test.go | 129 ++++++++++++------ 1 file changed, 84 insertions(+), 45 deletions(-) diff --git a/internal/credential/vault/repository_credential_library_test.go b/internal/credential/vault/repository_credential_library_test.go index 5751c3e6d..542d93396 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -1453,56 +1453,95 @@ func TestRepository_DeleteCredentialLibrary(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) - _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] - l := TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 1)[0] + { + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + l := TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 1)[0] - badId, err := newCredentialLibraryId() - require.NoError(t, err) - require.NotNil(t, badId) + badId, err := newCredentialLibraryId() + require.NoError(t, err) + require.NotNil(t, badId) - tests := []struct { - name string - in string - want int - wantErr errors.Code - }{ - { - name: "found", - in: l.GetPublicId(), - want: 1, - }, - { - name: "not-found", - in: badId, - }, - { - name: "empty-public-id", - in: "", - wantErr: errors.InvalidParameter, - }, + tests := []struct { + name string + in string + want int + wantErr errors.Code + }{ + { + name: "found", + in: l.GetPublicId(), + want: 1, + }, + { + name: "not-found", + in: badId, + }, + { + name: "empty-public-id", + in: "", + wantErr: errors.InvalidParameter, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + got, err := repo.DeleteCredentialLibrary(ctx, prj.GetPublicId(), tt.in) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + return + } + assert.NoError(err) + assert.Equal(tt.want, got, "row count") + }) + } } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - ctx := context.Background() - kms := kms.TestKms(t, conn, wrapper) - sche := scheduler.TestScheduler(t, conn, wrapper) - repo, err := NewRepository(rw, rw, kms, sche) - assert.NoError(err) - require.NotNil(repo) + t.Run("library-with-mapping-overrides", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) - got, err := repo.DeleteCredentialLibrary(ctx, prj.GetPublicId(), tt.in) - if tt.wantErr != 0 { - assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) - return - } - assert.NoError(err) - assert.Equal(tt.want, got, "row count") - }) - } + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + lib := &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("orig-username"), + WithOverridePasswordAttribute("orig-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + CredentialType: string(credential.UserPasswordType), + }, + } + + orig, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), lib) + assert.NoError(err) + require.NotNil(orig) + + // test + got, err := repo.DeleteCredentialLibrary(ctx, prj.GetPublicId(), orig.GetPublicId()) + assert.NoError(err) + assert.Equal(1, got) + }) } func TestRepository_ListCredentialLibraries(t *testing.T) { From b8afd4f50c54a257b27df7b614ceae26ef555cde Mon Sep 17 00:00:00 2001 From: Louis Ruch Date: Thu, 9 Dec 2021 14:49:54 -0800 Subject: [PATCH 12/12] feat(cli): Display credential type in library list --- internal/cmd/commands/credentiallibrariescmd/funcs.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/cmd/commands/credentiallibrariescmd/funcs.go b/internal/cmd/commands/credentiallibrariescmd/funcs.go index 9292a227c..9edd1166b 100644 --- a/internal/cmd/commands/credentiallibrariescmd/funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/funcs.go @@ -87,6 +87,11 @@ func (c *Command) printListTable(items []*credentiallibraries.CredentialLibrary) fmt.Sprintf(" Type: %s", m.Type), ) } + if m.CredentialType != "" { + output = append(output, + fmt.Sprintf(" Credential Type: %s", m.CredentialType), + ) + } if m.Name != "" { output = append(output, fmt.Sprintf(" Name: %s", m.Name),