diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 1029448fe8..7c1cc07b3d 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -32,9 +32,7 @@ const ( type Library interface { boundary.Resource GetStoreId() string - - // TODO(mgaffney) 10/2021: Add method for returning the credential type - // of the library + CredentialType() Type } // Purpose is the purpose of the credential. diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 12a27f203b..3936165512 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "strings" + "time" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db/timestamp" @@ -12,84 +13,153 @@ import ( "github.com/hashicorp/boundary/internal/kms" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" + vault "github.com/hashicorp/vault/api" "google.golang.org/protobuf/proto" ) -var _ credential.Dynamic = (*actualCredential)(nil) +var _ credential.UserPassword = (*usrPassCred)(nil) + +type usrPassCred struct { + *baseCred + username string + password credential.Password +} + +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 -type actualCredential struct { - id string - sessionId string lib *privateLibrary secretData map[string]interface{} - purpose credential.Purpose } -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 (bc *baseCred) Secret() credential.SecretData { return bc.secretData } +func (bc *baseCred) Library() credential.Library { return bc.lib } +func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.Purpose } +func (bc *baseCred) getExpiration() time.Duration { return bc.expiration } + +// convert converts bc to a specific credential type if bc is not +// UnspecifiedType. +func convert(ctx context.Context, bc *baseCred) (dynamicCred, error) { + switch bc.Library().CredentialType() { + case credential.UserPasswordType: + return baseToUsrPass(ctx, bc) + } + return bc, nil +} + +func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) { + switch { + case bc == nil: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred")) + case bc.lib == nil: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred.lib")) + case bc.Library().CredentialType() != credential.UserPasswordType: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type")) + } + + uAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PasswordAttribute + if uAttr == "" { + uAttr = "username" + } + if pAttr == "" { + pAttr = "password" + } + + var username, password string + if u, ok := bc.secretData[uAttr]; ok { + if u, ok := u.(string); ok { + username = u + } + } + if p, ok := bc.secretData[pAttr]; ok { + if p, ok := p.(string); ok { + password = p + } + } + + if username == "" || password == "" { + return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping)) + } + + return &usrPassCred{ + baseCred: bc, + username: username, + password: credential.Password(password), + }, nil +} var _ credential.Library = (*privateLibrary)(nil) +// A privateLibrary contains all the values needed to connect to Vault and +// retrieve credentials. type privateLibrary struct { - PublicId string `gorm:"primary_key"` - StoreId string - Name string - Description string - CreateTime *timestamp.Timestamp - UpdateTime *timestamp.Timestamp - Version uint32 - ScopeId string - VaultPath string - HttpMethod string - HttpRequestBody []byte - VaultAddress string - Namespace string - CaCert []byte - TlsServerName string - TlsSkipVerify bool - TokenHmac []byte - Token TokenSecret - CtToken []byte - TokenKeyId string - ClientCert []byte - ClientKey KeySecret - CtClientKey []byte - ClientKeyId string - Purpose credential.Purpose `gorm:"-"` + PublicId string `gorm:"primary_key"` + StoreId string + CredType string `gorm:"column:credential_type"` + UsernameAttribute string + PasswordAttribute string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + ScopeId string + VaultPath string + HttpMethod string + HttpRequestBody []byte + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + TokenHmac []byte + Token TokenSecret + CtToken []byte + TokenKeyId string + ClientCert []byte + ClientKey KeySecret + CtClientKey []byte + ClientKeyId string + Purpose credential.Purpose `gorm:"-"` } func (pl *privateLibrary) clone() *privateLibrary { // The 'append(a[:0:0], a...)' comes from // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F return &privateLibrary{ - PublicId: pl.PublicId, - StoreId: pl.StoreId, - Name: pl.Name, - Description: pl.Description, - CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), - UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), - Version: pl.Version, - ScopeId: pl.ScopeId, - VaultPath: pl.VaultPath, - HttpMethod: pl.HttpMethod, - HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...), - VaultAddress: pl.VaultAddress, - Namespace: pl.Namespace, - CaCert: append(pl.CaCert[:0:0], pl.CaCert...), - TlsServerName: pl.TlsServerName, - TlsSkipVerify: pl.TlsSkipVerify, - TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...), - Token: append(pl.Token[:0:0], pl.Token...), - CtToken: append(pl.CtToken[:0:0], pl.CtToken...), - TokenKeyId: pl.TokenKeyId, - ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...), - ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...), - CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), - ClientKeyId: pl.ClientKeyId, - Purpose: pl.Purpose, + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), + UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), + Version: pl.Version, + ScopeId: pl.ScopeId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...), + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: append(pl.CaCert[:0:0], pl.CaCert...), + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...), + Token: append(pl.Token[:0:0], pl.Token...), + CtToken: append(pl.CtToken[:0:0], pl.CtToken...), + TokenKeyId: pl.TokenKeyId, + ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...), + ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...), + CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, } } @@ -101,6 +171,15 @@ func (pl *privateLibrary) GetVersion() uint32 { return pl.Versi func (pl *privateLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } func (pl *privateLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } +func (pl *privateLibrary) CredentialType() credential.Type { + switch ct := pl.CredType; ct { + case "": + return credential.UnspecifiedType + default: + return credential.Type(ct) + } +} + func (pl *privateLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { const op = "vault.(privateLibrary).decrypt" @@ -157,6 +236,63 @@ func (pl *privateLibrary) client() (*client, error) { return client, nil } +type dynamicCred interface { + credential.Dynamic + getExpiration() time.Duration + insertQuery() (query string, queryValues []interface{}) + updateSessionQuery(purpose credential.Purpose) (query string, queryValues []interface{}) +} + +// retrieveCredential retrieves a dynamic credential from Vault for the +// given sessionId. +func (pl *privateLibrary) retrieveCredential(ctx context.Context, op errors.Op, sessionId string) (dynamicCred, error) { + // Get the credential ID early. No need to get a secret from Vault + // if there is no way to save it in the database. + credId, err := newCredentialId() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + client, err := pl.client() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var secret *vault.Secret + switch Method(pl.HttpMethod) { + case MethodGet: + secret, err = client.get(pl.VaultPath) + case MethodPost: + secret, err = client.post(pl.VaultPath, pl.HttpRequestBody) + default: + return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unknown http method: library: %s", pl.PublicId)) + } + + if err != nil { + // TODO(mgaffney) 05/2021: detect if the error is because of an + // expired or invalid token + return nil, errors.Wrap(ctx, err, op) + } + if secret == nil { + return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op)) + } + + leaseDuration := time.Duration(secret.LeaseDuration) * time.Second + cred, err := newCredential(pl.GetPublicId(), sessionId, secret.LeaseID, pl.TokenHmac, leaseDuration) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cred.PublicId = credId + cred.IsRenewable = secret.Renewable + + dCred := &baseCred{ + Credential: cred, + lib: pl, + secretData: secret.Data, + } + return convert(ctx, dCred) +} + // TableName returns the table name for gorm. func (pl *privateLibrary) TableName() string { return "credential_vault_library_private" diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index b2f925b082..fcc6f652df 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/scheduler" @@ -77,7 +78,7 @@ func TestRepository_getPrivateLibraries(t *testing.T) { assert.NotNil(origLookup.Token()) assert.Equal(origStore.GetPublicId(), origLookup.GetPublicId()) - libs := make(map[string]*CredentialLibrary, 3) + libs := make(map[string]*CredentialLibrary) var requests []credential.Request { libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path") @@ -112,6 +113,72 @@ func TestRepository_getPrivateLibraries(t *testing.T) { req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} requests = append(requests, req) } + { + opts := []Option{ + WithCredentialType(credential.UserPasswordType), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.UserPasswordType), + WithMappingOverride(NewUserPasswordOverride( + WithOverrideUsernameAttribute("test-username"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.UserPasswordType), + WithMappingOverride(NewUserPasswordOverride( + WithOverridePasswordAttribute("test-password"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.UserPasswordType), + WithMappingOverride(NewUserPasswordOverride( + WithOverrideUsernameAttribute("test-username"), + WithOverridePasswordAttribute("test-password"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } gotLibs, err := repo.getPrivateLibraries(ctx, requests) assert.NoError(err) @@ -131,6 +198,16 @@ func TestRepository_getPrivateLibraries(t *testing.T) { assert.Equal(want.VaultPath, got.VaultPath) assert.Equal(want.HttpMethod, got.HttpMethod) assert.Equal(want.HttpRequestBody, got.HttpRequestBody) + assert.Equal(want.CredentialType(), got.CredentialType()) + if mo := want.MappingOverride; mo != nil { + switch w := mo.(type) { + case *UserPasswordOverride: + assert.Equal(w.UsernameAttribute, got.UsernameAttribute) + assert.Equal(w.PasswordAttribute, got.PasswordAttribute) + default: + assert.Fail("unknown mapping override type") + } + } } }) } @@ -227,3 +304,179 @@ func TestRequestMap(t *testing.T) { }) } } + +func TestBaseToUsrPass(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + given *baseCred + want *usrPassCred + wantErr errors.Code + }{ + { + name: "nil-input", + wantErr: errors.InvalidParameter, + }, + { + name: "nil-library", + given: &baseCred{}, + wantErr: errors.InvalidParameter, + }, + { + name: "library-not-username-password-type", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UnspecifiedType), + }, + }, + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-no-username-default-password-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "password": "my-password", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-no-password-default-username-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "username": "my-username", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "valid-default-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + }, + secretData: map[string]interface{}{ + "username": "my-username", + "password": "my-password", + }, + }, + want: &usrPassCred{ + username: "my-username", + password: credential.Password("my-password"), + }, + }, + { + name: "valid-override-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "test-username", + PasswordAttribute: "test-password", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + want: &usrPassCred{ + username: "override-username", + password: credential.Password("override-password"), + }, + }, + { + name: "valid-default-username-override-password", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + PasswordAttribute: "test-password", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + want: &usrPassCred{ + username: "default-username", + password: credential.Password("override-password"), + }, + }, + { + name: "valid-override-username-default-password", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "test-username", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + want: &usrPassCred{ + username: "override-username", + password: credential.Password("default-password"), + }, + }, + { + name: "invalid-username-override", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "missing-username", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-password-override", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UserPasswordType), + UsernameAttribute: "missing-password", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "password": "default-password", + "test-username": "override-username", + "test-password": "override-password", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := baseToUsrPass(context.Background(), tt.given) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + want := tt.want + want.baseCred = tt.given + assert.Equal(want, got) + }) + } +} diff --git a/internal/credential/vault/repository_credentials.go b/internal/credential/vault/repository_credentials.go index af19a2e72a..575d1d6625 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 5078b2418e..7689f661a5 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -64,6 +64,8 @@ func TestRepository_IssueCredentials(t *testing.T) { type libT int const ( libDB libT = iota + libUsrPassDB + libErrUsrPassDB libPKI libErrPKI libKV @@ -105,23 +107,53 @@ func TestRepository_IssueCredentials(t *testing.T) { { libPath := path.Join("secret", "data", "my-secret") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) - assert.NoError(err) - require.NotNil(libIn) + assert.NoError(t, err) + require.NotNil(t, libIn) lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) - assert.NoError(err) - require.NotNil(lib) + assert.NoError(t, err) + require.NotNil(t, lib) libs[libKV] = lib.GetPublicId() } { libPath := path.Join("secret", "data", "fake-secret") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) - assert.NoError(err) - require.NotNil(libIn) + assert.NoError(t, err) + require.NotNil(t, libIn) lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) - assert.NoError(err) - require.NotNil(lib) + assert.NoError(t, err) + require.NotNil(t, lib) libs[libErrKV] = lib.GetPublicId() } + { + libPath := path.Join("database", "creds", "opened") + opts := []vault.Option{ + vault.WithCredentialType(credential.UserPasswordType), + } + libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) + assert.NoError(t, err) + require.NotNil(t, libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(t, err) + require.NotNil(t, lib) + libs[libUsrPassDB] = lib.GetPublicId() + } + { + libPath := path.Join("database", "creds", "opened") + opts := []vault.Option{ + vault.WithCredentialType(credential.UserPasswordType), + vault.WithMappingOverride(vault.NewUserPasswordOverride( + vault.WithOverrideUsernameAttribute("test-username"), + vault.WithOverridePasswordAttribute("test-password"), + )), + } + libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) + assert.NoError(t, err) + require.NotNil(t, libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(t, err) + require.NotNil(t, lib) + libs[libErrUsrPassDB] = lib.GetPublicId() + } at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) uId := at.GetIamUserId() @@ -212,6 +244,16 @@ func TestRepository_IssueCredentials(t *testing.T) { }, }, }, + { + name: "one-valid-username-password-library", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libUsrPassDB], + Purpose: credential.ApplicationPurpose, + }, + }, + }, { name: "invalid-kv-does-not-exist", convertFn: rc2dc, @@ -223,6 +265,17 @@ func TestRepository_IssueCredentials(t *testing.T) { }, wantErr: errors.VaultEmptySecret, }, + { + name: "one-valid-username-password-library", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libErrUsrPassDB], + Purpose: credential.ApplicationPurpose, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, } for _, tt := range tests { tt := tt @@ -245,7 +298,23 @@ func TestRepository_IssueCredentials(t *testing.T) { return } assert.Len(got, len(tt.requests)) - assert.NoError(err) + require.NoError(err) + assert.NotZero(len(got)) + for _, dc := range got { + switch dc.Library().CredentialType() { + case credential.UserPasswordType: + if upc, ok := dc.(credential.UserPassword); ok { + assert.NotEmpty(upc.Username()) + assert.NotEmpty(upc.Password()) + break + } + assert.Fail("want UserPassword credential from library with credential type UserPassword") + case credential.UnspecifiedType: + if _, ok := dc.(credential.UserPassword); ok { + assert.Fail("do not want UserPassword credential from library with credential type Unspecified") + } + } + } }) } } diff --git a/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql b/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql new file mode 100644 index 0000000000..ac9fa30c2e --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/22/05_vault_private_library.up.sql @@ -0,0 +1,48 @@ +begin; + + drop view credential_vault_library_private; + + create view credential_vault_library_private as + with + password_override (library_id, username_attribute, password_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_user_password_mapping_override + ) + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + library.http_method as http_method, + library.http_request_body as http_request_body, + library.credential_type as credential_type, + store.scope_id as scope_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.token_hmac as token_hmac, + store.ct_token as ct_token, -- encrypted + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + upasso.username_attribute as username_attribute, + upasso.password_attribute as password_attribute + from credential_vault_library library + join credential_vault_store_private store + on library.store_id = store.public_id + left join password_override upasso + on library.public_id = upasso.library_id + and store.token_status = 'current'; + comment on view credential_vault_library_private is + 'credential_vault_library_private is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' + 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + +commit; diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 50e092b282..21603875cd 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -313,6 +313,11 @@ begin; values ('p____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default'); + insert into credential_vault_token + (store_id, key_id, status, token_hmac, token, last_renewal_time, expiration_time) + values + ('vs_______wvs', 'kdkv___widget', 'current', 'hmac-value', 'token-value', now(), now() + interval '1 hour'); + insert into credential_vault_library (store_id, public_id, name, description, vault_path, http_method, credential_type) values diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql index a0e7186b3c..139992c87b 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_user_password_mapping_override.sql @@ -4,7 +4,7 @@ -- delete_credential_vault_library_mapping_override_subtype begin; - select plan(10); + select plan(11); select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); -- validate the setup data @@ -16,6 +16,23 @@ begin; from credential_vault_library_mapping_override where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); + prepare select_private_libraries as + select public_id::text, credential_type::text, username_attribute::text, password_attribute::text + from credential_vault_library_private + where public_id in ('vl______wvl2', 'vl______wvl3', 'vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7') + order by public_id; + + select results_eq( + 'select_private_libraries', + $$VALUES + ('vl______wvl2', 'unspecified', null, null), + ('vl______wvl3', 'user_password', null, null), + ('vl______wvl4', 'user_password', null, null), + ('vl______wvl5', 'user_password', 'my_username', null), + ('vl______wvl6', 'user_password', null, 'my_password'), + ('vl______wvl7', 'user_password', 'my_username', 'my_password')$$ + ); + -- validate the insert triggers select is(count(*), 0::bigint) from credential_vault_library_user_password_mapping_override where library_id = 'vl______wvl3'; select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3'; diff --git a/internal/errors/code.go b/internal/errors/code.go index f343de31b1..8ff7681bb9 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -109,6 +109,7 @@ const ( VaultCredentialRequest Code = 3014 // VaultCredentialRequest represents an error returned from Vault when retrieving a credential VaultEmptySecret Code = 3015 // VaultEmptySecret represents a empty secret was returned from Vault without error VaultInvalidMappingOverride Code = 3016 // VaultInvalidMappingOverride represents an error returned when a credential mapping is unknown or does not match a credential type + VaultInvalidCredentialMapping Code = 3017 // VaultInvalidCredentialMapping represents an error returned when a Vault secret failed to be mapped to a specific credential type // OIDC authentication provided errors OidcProviderCallbackError Code = 4000 // OidcProviderCallbackError represents an error that is passed by the OIDC provider to the callback endpoint diff --git a/internal/errors/code_test.go b/internal/errors/code_test.go index 7f80a2aba7..8bc005f79a 100644 --- a/internal/errors/code_test.go +++ b/internal/errors/code_test.go @@ -292,6 +292,11 @@ func TestCode_Both_String_Info(t *testing.T) { c: VaultInvalidMappingOverride, want: VaultInvalidMappingOverride, }, + { + name: "VaultInvalidCredentialMapping", + c: VaultInvalidCredentialMapping, + want: VaultInvalidCredentialMapping, + }, { name: "OidcProviderCallbackError", c: OidcProviderCallbackError, diff --git a/internal/errors/info.go b/internal/errors/info.go index 31e6aafda5..9038edc540 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -236,6 +236,10 @@ var errorCodeInfo = map[Code]Info{ Message: "invalid credential mapping override", Kind: Parameter, }, + VaultInvalidCredentialMapping: { + Message: "mapping vault secret to a credential type failed", + Kind: Integrity, + }, OidcProviderCallbackError: { Message: "oidc provider callback error", Kind: External,