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..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), @@ -168,6 +173,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..6648c3cc0 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,9 +34,16 @@ func extraVaultActionsFlagsMapFuncImpl() map[string][]string { pathFlagName, httpMethodFlagName, httpRequestBodyFlagName, + credentialTypeFlagName, + credentialMappingFlagName, + }, + "update": { + pathFlagName, + httpMethodFlagName, + httpRequestBodyFlagName, + credentialMappingFlagName, }, } - flags["update"] = flags["create"] return flags } @@ -59,11 +70,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 +108,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.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 + 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/credential.go b/internal/credential/credential.go index 5c337ae21..7c1cc07b3 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -18,11 +18,21 @@ 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 + CredentialType() Type } // 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..c6e165b7e 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,63 @@ 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), + }, + }, + }, + { + 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 { @@ -198,17 +255,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/fields.go b/internal/credential/vault/fields.go index bd2415dd8..42826fdb6 100644 --- a/internal/credential/vault/fields.go +++ b/internal/credential/vault/fields.go @@ -17,4 +17,8 @@ const ( tlsServerNameField = "TlsServerName" tlsSkipVerifyField = "TlsSkipVerify" tokenField = "Token" + + // MappingOverrideField represents the field mask indicating a mapping override + // update has been requested. + MappingOverrideField = "MappingOverride" ) 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/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/private_library.go b/internal/credential/vault/private_library.go index 12a27f203..f0392b29e 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -5,91 +5,150 @@ import ( "database/sql" "fmt" "strings" + "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" 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" + } + username, password := userpassword.Extract(bc.secretData, uAttr, pAttr) + 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 +160,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 +225,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..c2f71b62f 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,368 @@ 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, + }, + { + 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 + 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_credential_library.go b/internal/credential/vault/repository_credential_library.go index d28727664..3e3ffb2df 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" @@ -48,11 +49,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 +67,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 }, ) @@ -86,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 @@ -113,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): @@ -120,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) } @@ -132,12 +163,19 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, scopeId string vaultPathField: l.VaultPath, httpMethodField: l.HttpMethod, httpRequestBodyField: l.HttpRequestBody, + MappingOverrideField: l.MappingOverride, }, fieldMaskPaths, nil, ) 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) @@ -147,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), @@ -156,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 }, ) @@ -187,7 +329,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) { @@ -195,9 +337,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 039675242..542d93396 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -5,8 +5,7 @@ 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" dbassert "github.com/hashicorp/boundary/internal/db/assert" @@ -17,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) { @@ -166,6 +164,127 @@ 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-username-override", + in: &CredentialLibrary{ + MappingOverride: NewUserPasswordOverride( + WithOverrideUsernameAttribute("utest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + want: &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), + }, + }, + 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", + VaultPath: "/some/path", + CredentialType: string(credential.UserPasswordType), + }, + }, + }, } for _, tt := range tests { @@ -191,8 +310,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())) + } }) } @@ -322,6 +461,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 @@ -366,6 +519,7 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { wantCount int wantErr errors.Code }{ + { name: "nil-credential-library", orig: &CredentialLibrary{ @@ -604,17 +758,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, @@ -757,6 +915,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 { @@ -798,19 +1146,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") + } }) } @@ -935,64 +1300,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) { @@ -1001,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) { @@ -1059,59 +1550,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) { 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 3ddc2b1db..978663109 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -36,94 +36,138 @@ 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"})) - // 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 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 ( libDB libT = iota + libUsrPassDB + libErrUsrPassDB libPKI libErrPKI libKV libErrKV + libUsrPassKV ) libs := make(map[libT]string) { 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() } { 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() + } + { + 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() @@ -214,6 +258,16 @@ func TestRepository_IssueCredentials(t *testing.T) { }, }, }, + { + name: "one-db-valid-username-password-library", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libUsrPassDB], + Purpose: credential.ApplicationPurpose, + }, + }, + }, { name: "invalid-kv-does-not-exist", convertFn: rc2dc, @@ -225,10 +279,32 @@ func TestRepository_IssueCredentials(t *testing.T) { }, wantErr: errors.VaultEmptySecret, }, + { + name: "invalid-username-password-library", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libErrUsrPassDB], + Purpose: credential.ApplicationPurpose, + }, + }, + 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 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(), @@ -246,7 +322,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/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/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/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/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/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..b0964bbf8 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql @@ -0,0 +1,68 @@ +begin; + +-- Replaces view from 10/04_vault_credential.up.sql + 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.'; + + 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; 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..21603875c 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -313,12 +313,41 @@ begin; values ('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) + insert into credential_vault_token + (store_id, key_id, status, token_hmac, token, last_renewal_time, expiration_time) 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', '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 + ('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..139992c87 --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql @@ -0,0 +1,61 @@ +-- 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(11); + 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'); + + 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'; + + 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..8ff7681bb 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -108,6 +108,8 @@ 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 + 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 129a4970f..8bc005f79 100644 --- a/internal/errors/code_test.go +++ b/internal/errors/code_test.go @@ -287,6 +287,16 @@ func TestCode_Both_String_Info(t *testing.T) { c: VaultEmptySecret, want: VaultEmptySecret, }, + { + name: "VaultInvalidMappingOverride", + 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 e1f317755..9038edc54 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -232,6 +232,14 @@ var errorCodeInfo = map[Code]Info{ Message: "vault secret is empty", Kind: Integrity, }, + VaultInvalidMappingOverride: { + 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, 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/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; +} diff --git a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go index 31dc178eb..349b61295 100644 --- a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -21,13 +21,22 @@ 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 +const ( + usernameAttribute string = "username_attribute" + passwordAttribute string = "password_attribute" ) var ( @@ -220,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 } @@ -319,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")) } @@ -463,13 +505,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 +576,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 +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, credential.Type(req.GetItem().GetCredentialType()), req.GetItem().CredentialMappingOverrides.AsMap()) default: badFields[globals.CredentialStoreIdField] = "This field must be a valid credential store id." } @@ -556,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()) { @@ -564,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." @@ -578,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) @@ -600,3 +687,76 @@ func validateListRequest(req *pbs.ListCredentialLibrariesRequest) error { } return nil } + +func validateMapping(badFields map[string]string, credentialType credential.Type, overrides map[string]interface{}) { + validFields := make(map[string]bool) + 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) + } + 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) + } + } +} + +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 681294a1a..38a890296 100644 --- a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -1,12 +1,14 @@ package credentiallibraries import ( + "context" "fmt" "sort" "strings" "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 +328,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 +446,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) { @@ -461,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 @@ -473,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 @@ -495,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), @@ -612,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) @@ -629,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 }{ @@ -666,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 }, @@ -688,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 @@ -714,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 { @@ -782,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) { 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() }