From 7833dd73c07144ee93e42ac958ae7363b7c82450 Mon Sep 17 00:00:00 2001 From: Danielle <29378233+DanielleMiu@users.noreply.github.com> Date: Mon, 9 Jan 2023 12:06:43 -0500 Subject: [PATCH] refact(vault): Create issuingCredentialLibrary Interface Allows for new issuing credential library implementations in the future. Moved db and decrypt logic to privateCredentialLibraryAllTypes from issueCredentialLibrary. This also removes sessionId from retrieveCredential, it was not needed and removing allows retrieveCredential to be reused for credentials issued outside current workflow. --- internal/credential/vault/credential.go | 40 +-- internal/credential/vault/credential_test.go | 16 +- internal/credential/vault/private_library.go | 241 +++++++++++++----- .../credential/vault/private_library_test.go | 93 +++---- .../vault/repository_credentials.go | 49 +++- .../vault/repository_credentials_test.go | 58 +++++ internal/credential/vault/testing.go | 4 +- 7 files changed, 338 insertions(+), 163 deletions(-) diff --git a/internal/credential/vault/credential.go b/internal/credential/vault/credential.go index 10f390f8e..5ca58ece9 100644 --- a/internal/credential/vault/credential.go +++ b/internal/credential/vault/credential.go @@ -1,10 +1,8 @@ package vault import ( - "database/sql" "time" - "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" @@ -46,14 +44,11 @@ type Credential struct { expiration time.Duration `gorm:"-"` } -func newCredential(libraryId, sessionId, externalId string, tokenHmac []byte, expiration time.Duration) (*Credential, error) { +func newCredential(libraryId, externalId string, tokenHmac []byte, expiration time.Duration) (*Credential, error) { const op = "vault.newCredential" if libraryId == "" { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no library id") } - if sessionId == "" { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no session id") - } if len(tokenHmac) == 0 { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no tokenHmac") } @@ -69,7 +64,6 @@ func newCredential(libraryId, sessionId, externalId string, tokenHmac []byte, ex expiration: expiration.Round(time.Second), Credential: &store.Credential{ LibraryId: libraryId, - SessionId: sessionId, ExternalId: externalId, TokenHmac: tokenHmac, Status: status, @@ -117,38 +111,6 @@ func (c *Credential) oplog(op oplog.OpType) oplog.Metadata { return metadata } -func (c *Credential) insertQuery() (query string, queryValues []any) { - queryValues = []any{ - sql.Named("public_id", c.PublicId), - sql.Named("library_id", c.LibraryId), - sql.Named("session_id", c.SessionId), - sql.Named("token_hmac", c.TokenHmac), - sql.Named("external_id", c.ExternalId), - sql.Named("is_renewable", c.IsRenewable), - sql.Named("status", c.Status), - sql.Named("last_renewal_time", "now()"), - } - switch { - case c.expiration == 0: - query = insertCredentialWithInfiniteExpirationQuery - default: - query = insertCredentialWithExpirationQuery - queryValues = append(queryValues, sql.Named("expiration_time", int(c.expiration.Round(time.Second).Seconds()))) - } - return -} - -func (c *Credential) updateSessionQuery(purpose credential.Purpose) (query string, queryValues []any) { - queryValues = []any{ - sql.Named("public_id", c.PublicId), - sql.Named("library_id", c.LibraryId), - sql.Named("session_id", c.SessionId), - sql.Named("purpose", string(purpose)), - } - query = updateSessionCredentialQuery - return -} - func (c *Credential) updateExpirationQuery() (query string, queryValues []any) { queryValues = []any{ int(c.expiration.Round(time.Second).Seconds()), diff --git a/internal/credential/vault/credential_test.go b/internal/credential/vault/credential_test.go index 07b6e795b..9666d9f4e 100644 --- a/internal/credential/vault/credential_test.go +++ b/internal/credential/vault/credential_test.go @@ -56,17 +56,6 @@ func TestCredential_New(t *testing.T) { want: nil, wantErr: true, }, - { - name: "missing-session-id", - args: args{ - libraryId: lib.GetPublicId(), - externalId: "some/vault/credential", - tokenHmac: token.GetTokenHmac(), - expiration: 5 * time.Minute, - }, - want: nil, - wantErr: true, - }, { name: "missing-tokenHmac", args: args{ @@ -144,7 +133,7 @@ func TestCredential_New(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) ctx := context.Background() - got, err := newCredential(tt.args.libraryId, tt.args.sessionId, + got, err := newCredential(tt.args.libraryId, tt.args.externalId, tt.args.tokenHmac, tt.args.expiration) if tt.wantErr { assert.Error(err) @@ -162,7 +151,8 @@ func TestCredential_New(t *testing.T) { tt.want.PublicId = id got.PublicId = id - query, queryValues := got.insertQuery() + query, queryValues := insertQuery(got, tt.args.sessionId) + require.NoError(err) rows, err2 := rw.Exec(ctx, query, queryValues) assert.Equal(1, rows) diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 274194126..712b84765 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -25,7 +25,7 @@ var _ credential.Dynamic = (*baseCred)(nil) type baseCred struct { *Credential - lib *issueCredentialLibrary + lib *genericIssuingCredentialLibrary secretData map[string]any } @@ -33,6 +33,7 @@ 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 } +func (bc *baseCred) getCredential() *Credential { return bc.Credential } // convert converts bc to a specific credential type if bc is not // UnspecifiedType. @@ -132,12 +133,17 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err }, nil } -var _ credential.Library = (*issueCredentialLibrary)(nil) +var _ credential.Library = (*genericIssuingCredentialLibrary)(nil) -// A issueCredentialLibrary contains all the values needed to connect to Vault and +type issuingCredentialLibrary interface { + GetPublicId() string + retrieveCredential(context.Context, errors.Op, ...credential.Option) (dynamicCred, error) +} + +// A genericIssuingCredentialLibrary contains all the values needed to connect to Vault and // retrieve credentials. -type issueCredentialLibrary struct { - PublicId string `gorm:"primary_key"` +type genericIssuingCredentialLibrary struct { + PublicId string StoreId string Name string Description string @@ -147,7 +153,7 @@ type issueCredentialLibrary struct { VaultPath string HttpMethod string HttpRequestBody []byte - CredType string `gorm:"column:credential_type"` + CredType string ProjectId string VaultAddress string Namespace string @@ -167,13 +173,13 @@ type issueCredentialLibrary struct { PasswordAttribute string PrivateKeyAttribute string PrivateKeyPassphraseAttribute string - Purpose credential.Purpose `gorm:"-"` + Purpose credential.Purpose } -func (pl *issueCredentialLibrary) clone() *issueCredentialLibrary { +func (pl *genericIssuingCredentialLibrary) clone() *genericIssuingCredentialLibrary { // The 'append(a[:0:0], a...)' comes from // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F - return &issueCredentialLibrary{ + return &genericIssuingCredentialLibrary{ PublicId: pl.PublicId, StoreId: pl.StoreId, CredType: pl.CredType, @@ -208,15 +214,15 @@ func (pl *issueCredentialLibrary) clone() *issueCredentialLibrary { } } -func (pl *issueCredentialLibrary) GetPublicId() string { return pl.PublicId } -func (pl *issueCredentialLibrary) GetStoreId() string { return pl.StoreId } -func (pl *issueCredentialLibrary) GetName() string { return pl.Name } -func (pl *issueCredentialLibrary) GetDescription() string { return pl.Description } -func (pl *issueCredentialLibrary) GetVersion() uint32 { return pl.Version } -func (pl *issueCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } -func (pl *issueCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } +func (pl *genericIssuingCredentialLibrary) GetPublicId() string { return pl.PublicId } +func (pl *genericIssuingCredentialLibrary) GetStoreId() string { return pl.StoreId } +func (pl *genericIssuingCredentialLibrary) GetName() string { return pl.Name } +func (pl *genericIssuingCredentialLibrary) GetDescription() string { return pl.Description } +func (pl *genericIssuingCredentialLibrary) GetVersion() uint32 { return pl.Version } +func (pl *genericIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } +func (pl *genericIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } -func (pl *issueCredentialLibrary) CredentialType() credential.Type { +func (pl *genericIssuingCredentialLibrary) CredentialType() credential.Type { switch ct := pl.CredType; ct { case "": return credential.UnspecifiedType @@ -225,41 +231,8 @@ func (pl *issueCredentialLibrary) CredentialType() credential.Type { } } -func (pl *issueCredentialLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { - const op = "vault.(issueCredentialLibrary).decrypt" - - if pl.CtToken != nil { - type ptk struct { - Token []byte `wrapping:"pt,token_data"` - CtToken []byte `wrapping:"ct,token_data"` - } - ptkv := &ptk{ - CtToken: pl.CtToken, - } - if err := structwrapping.UnwrapStruct(ctx, cipher, ptkv, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("token")) - } - pl.Token = ptkv.Token - } - - if pl.CtClientKey != nil && pl.ClientCert != nil { - type pck struct { - Key []byte `wrapping:"pt,key_data"` - CtKey []byte `wrapping:"ct,key_data"` - } - pckv := &pck{ - CtKey: pl.CtClientKey, - } - if err := structwrapping.UnwrapStruct(ctx, cipher, pckv, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("client certificate")) - } - pl.ClientKey = pckv.Key - } - return nil -} - -func (pl *issueCredentialLibrary) client(ctx context.Context) (vaultClient, error) { - const op = "vault.(issueCredentialLibrary).client" +func (pl *genericIssuingCredentialLibrary) client(ctx context.Context) (vaultClient, error) { + const op = "vault.(genericIssuingCredentialLibrary).client" clientConfig := &clientConfig{ Addr: pl.VaultAddress, Token: pl.Token, @@ -284,15 +257,14 @@ func (pl *issueCredentialLibrary) client(ctx context.Context) (vaultClient, erro type dynamicCred interface { credential.Dynamic getExpiration() time.Duration - insertQuery() (query string, queryValues []any) - updateSessionQuery(purpose credential.Purpose) (query string, queryValues []any) + getCredential() *Credential } // retrieveCredential retrieves a dynamic credential from Vault for the // given sessionId. // // Supported options: credential.WithTemplateData -func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, sessionId string, opt ...credential.Option) (dynamicCred, error) { +func (pl *genericIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, opt ...credential.Option) (dynamicCred, error) { opts, err := credential.GetOpts(opt...) if err != nil { return nil, errors.Wrap(ctx, err, op) @@ -358,7 +330,7 @@ func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op err } leaseDuration := time.Duration(secret.LeaseDuration) * time.Second - cred, err := newCredential(pl.GetPublicId(), sessionId, secret.LeaseID, pl.TokenHmac, leaseDuration) + cred, err := newCredential(pl.GetPublicId(), secret.LeaseID, pl.TokenHmac, leaseDuration) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -374,11 +346,11 @@ func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op err } // TableName returns the table name for gorm. -func (pl *issueCredentialLibrary) TableName() string { +func (pl *genericIssuingCredentialLibrary) TableName() string { return "credential_vault_library_issue_credentials" } -func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]*issueCredentialLibrary, error) { +func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]issuingCredentialLibrary, error) { const op = "vault.(Repository).getIssueCredLibraries" mapper, err := newMapper(requests) @@ -405,9 +377,9 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede } defer rows.Close() - var libs []*issueCredentialLibrary + var libs []*privateCredentialLibraryAllTypes for rows.Next() { - var lib issueCredentialLibrary + var lib privateCredentialLibraryAllTypes if err := r.reader.ScanRows(ctx, rows, &lib); err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("scan row failed")) } @@ -422,6 +394,7 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede } } + var decryptedLibs []issuingCredentialLibrary for _, pl := range libs { databaseWrapper, err := r.kms.GetWrapper(ctx, pl.ProjectId, kms.KeyPurposeDatabase) if err != nil { @@ -431,9 +404,155 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede if err := pl.decrypt(ctx, databaseWrapper); err != nil { return nil, errors.Wrap(ctx, err, op) } + decryptedLibs = append(decryptedLibs, pl.toTypedIssuingCredentialLibrary()) } - return libs, nil + return decryptedLibs, nil +} + +// privateCredentialLibraryAllTypes is a clone of genericIssuingCredentialLibrary. Contains +// all the values needed to connect to Vault and retrieve credentials. +type privateCredentialLibraryAllTypes 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 + CredType string `gorm:"column:credential_type"` + ProjectId string + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + WorkerFilter string + Token TokenSecret + CtToken []byte + TokenHmac []byte + TokenKeyId string + ClientCert []byte + ClientKey KeySecret + CtClientKey []byte + ClientKeyId string + UsernameAttribute string + PasswordAttribute string + PrivateKeyAttribute string + PrivateKeyPassphraseAttribute string + Purpose credential.Purpose `gorm:"-"` +} + +func (pl *privateCredentialLibraryAllTypes) GetPublicId() string { return pl.PublicId } + +func (pl *privateCredentialLibraryAllTypes) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "vault.(privateCredentialLibraryAllTypes).decrypt" + + if pl.CtToken != nil { + type ptk struct { + Token []byte `wrapping:"pt,token_data"` + CtToken []byte `wrapping:"ct,token_data"` + } + ptkv := &ptk{ + CtToken: pl.CtToken, + } + if err := structwrapping.UnwrapStruct(ctx, cipher, ptkv, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("token")) + } + pl.Token = ptkv.Token + } + + if pl.CtClientKey != nil && pl.ClientCert != nil { + type pck struct { + Key []byte `wrapping:"pt,key_data"` + CtKey []byte `wrapping:"ct,key_data"` + } + pckv := &pck{ + CtKey: pl.CtClientKey, + } + if err := structwrapping.UnwrapStruct(ctx, cipher, pckv, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("client certificate")) + } + pl.ClientKey = pckv.Key + } + return nil +} + +func (pl *privateCredentialLibraryAllTypes) clone() *privateCredentialLibraryAllTypes { + // The 'append(a[:0:0], a...)' comes from + // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F + return &privateCredentialLibraryAllTypes{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + PrivateKeyAttribute: pl.PrivateKeyAttribute, + PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), + UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), + Version: pl.Version, + ProjectId: pl.ProjectId, + 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, + WorkerFilter: pl.WorkerFilter, + 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, + } +} + +func (pl *privateCredentialLibraryAllTypes) toTypedIssuingCredentialLibrary() issuingCredentialLibrary { + return &genericIssuingCredentialLibrary{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + PrivateKeyAttribute: pl.PrivateKeyAttribute, + PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: pl.CreateTime, + UpdateTime: pl.UpdateTime, + Version: pl.Version, + ProjectId: pl.ProjectId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: pl.HttpRequestBody, + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: pl.CaCert, + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + WorkerFilter: pl.WorkerFilter, + TokenHmac: pl.TokenHmac, + Token: pl.Token, + CtToken: pl.CtToken, + TokenKeyId: pl.TokenKeyId, + ClientCert: pl.ClientCert, + ClientKey: pl.ClientKey, + CtClientKey: pl.CtClientKey, + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, + } } // requestMap takes a slice of credential requests and provides of list of diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index fc156232b..dca8b671c 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -269,7 +269,10 @@ func TestRepository_getPrivateLibraries(t *testing.T) { require.NotNil(gotLibs) assert.Len(gotLibs, len(libs)) - for _, got := range gotLibs { + for _, gotLib := range gotLibs { + got, ok := gotLib.(*genericIssuingCredentialLibrary) + require.True(ok) + assert.Equal(TokenSecret(token), got.Token) if tt.tls == TestClientTLS { require.NotNil(got.ClientKey) @@ -414,7 +417,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "library-not-username-password-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UnspecifiedType), }, }, @@ -423,7 +426,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-no-username-default-password-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -435,7 +438,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-no-password-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -447,7 +450,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -463,7 +466,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", PasswordAttribute: "test-password", @@ -483,7 +486,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-default-username-override-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), PasswordAttribute: "test-password", }, @@ -502,7 +505,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-override-username-default-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", }, @@ -521,7 +524,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-username-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "missing-username", }, @@ -537,7 +540,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-password-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "missing-password", }, @@ -553,7 +556,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-metadata-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -568,7 +571,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-data-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -580,7 +583,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-username-default-password-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -595,7 +598,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-passsword-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -610,7 +613,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -626,7 +629,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -639,7 +642,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-additional-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -656,7 +659,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -675,7 +678,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", PasswordAttribute: "test-password", @@ -698,7 +701,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-default-username-override-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), PasswordAttribute: "test-password", }, @@ -720,7 +723,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-override-username-default-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", }, @@ -779,7 +782,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "library-not-ssh-private-key-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UnspecifiedType), }, }, @@ -788,7 +791,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-no-username-default-pk-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -800,7 +803,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-no-pk-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -812,7 +815,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -828,7 +831,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -846,7 +849,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -866,7 +869,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -890,7 +893,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-username-override-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), PrivateKeyAttribute: "test-pk", }, @@ -909,7 +912,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-username-default-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", }, @@ -928,7 +931,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-username-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "missing-username", }, @@ -944,7 +947,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-pk-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "missing-pk", }, @@ -960,7 +963,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-metadata-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -975,7 +978,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-data-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -987,7 +990,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-username-default-pk-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1002,7 +1005,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-pk-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1017,7 +1020,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1033,7 +1036,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-invalid-data-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1046,7 +1049,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-additional-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1063,7 +1066,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1082,7 +1085,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1103,7 +1106,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -1126,7 +1129,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -1153,7 +1156,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-username-override-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), PrivateKeyAttribute: "test-pk", }, @@ -1175,7 +1178,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-username-default-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", }, diff --git a/internal/credential/vault/repository_credentials.go b/internal/credential/vault/repository_credentials.go index 8d72895ac..ab715847e 100644 --- a/internal/credential/vault/repository_credentials.go +++ b/internal/credential/vault/repository_credentials.go @@ -2,6 +2,7 @@ package vault import ( "context" + "database/sql" "fmt" "time" @@ -13,6 +14,38 @@ import ( var _ credential.Issuer = (*Repository)(nil) +func insertQuery(c *Credential, sessionId string) (query string, queryValues []any) { + queryValues = []any{ + sql.Named("public_id", c.PublicId), + sql.Named("library_id", c.LibraryId), + sql.Named("session_id", sessionId), + sql.Named("token_hmac", c.TokenHmac), + sql.Named("external_id", c.ExternalId), + sql.Named("is_renewable", c.IsRenewable), + sql.Named("status", c.Status), + sql.Named("last_renewal_time", "now()"), + } + switch { + case c.expiration == 0: + query = insertCredentialWithInfiniteExpirationQuery + default: + query = insertCredentialWithExpirationQuery + queryValues = append(queryValues, sql.Named("expiration_time", int(c.expiration.Round(time.Second).Seconds()))) + } + return +} + +func updateSessionQuery(c *Credential, sessionId string, purpose credential.Purpose) (query string, queryValues []any) { + queryValues = []any{ + sql.Named("public_id", c.PublicId), + sql.Named("library_id", c.LibraryId), + sql.Named("session_id", sessionId), + sql.Named("purpose", string(purpose)), + } + query = updateSessionCredentialQuery + return +} + // Issue issues and returns dynamic credentials from Vault for all of the // requests and assigns them to sessionId. func (r *Repository) Issue(ctx context.Context, sessionId string, requests []credential.Request, opt ...credential.Option) ([]credential.Dynamic, error) { @@ -37,7 +70,7 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre var minLease time.Duration runJobsInterval := r.scheduler.GetRunJobsInterval() for _, lib := range libs { - cred, err := lib.retrieveCredential(ctx, op, sessionId, opt...) + cred, err := lib.retrieveCredential(ctx, op, opt...) if err != nil { return nil, err } @@ -54,8 +87,18 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre if minLease > cred.getExpiration() { minLease = cred.getExpiration() } - insertQuery, insertQueryValues := cred.insertQuery() - updateQuery, updateQueryValues := cred.updateSessionQuery(lib.Purpose) + + underlyingCred := cred.getCredential() + + insertQuery, insertQueryValues := insertQuery(underlyingCred, sessionId) + if err != nil { + return nil, err + } + updateQuery, updateQueryValues := updateSessionQuery(underlyingCred, sessionId, cred.Purpose()) + if err != nil { + return nil, err + } + if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error { rowsInserted, err := w.Exec(ctx, insertQuery, insertQueryValues) diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 998ec15ee..0d66bf1b9 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" @@ -21,6 +22,58 @@ import ( "github.com/stretchr/testify/require" ) +// since we're not using gorm tags to retrieve this, it's faster and easier to just make a new struct than import usrPassCred from the vault package +type userPassCred struct { + PublicId string + LibraryId string + SessionId string + TokenHmac []byte + ExternalId string + IsRenewable bool + Status string + LastRenewalTime *timestamp.Timestamp + ExpirationTime *timestamp.Timestamp +} + +func lookupDbCred(t *testing.T, ctx context.Context, rw *db.Db, upc credential.UsernamePassword) userPassCred { + rows, err := rw.Query(ctx, ` + select + public_id, + library_id, + session_id, + token_hmac, + external_id, + is_renewable, + status, + last_renewal_time, + expiration_time + from credential_vault_credential + where public_id = ?; + `, []any{upc.GetPublicId()}) + require.NoError(t, err) + rowCount := 0 + + got := userPassCred{} + + for rows.Next() { + rowCount++ + require.NoError(t, rows.Scan( + &got.PublicId, + &got.LibraryId, + &got.SessionId, + &got.TokenHmac, + &got.ExternalId, + &got.IsRenewable, + &got.Status, + &got.LastRenewalTime, + &got.ExpirationTime, + )) + } + assert.Equal(t, 1, rowCount) + + return got +} + func TestRepository_IssueCredentials(t *testing.T) { t.Parallel() ctx := context.Background() @@ -398,6 +451,11 @@ func TestRepository_IssueCredentials(t *testing.T) { if upc, ok := dc.(credential.UsernamePassword); ok { assert.NotEmpty(upc.Username()) assert.NotEmpty(upc.Password()) + // we also want to retrieve the cred from the db and make sure it's the same as the one returned by Issue + retrieved := lookupDbCred(t, ctx, rw, upc) + require.Equal(retrieved.SessionId, sess.GetPublicId()) + require.Equal(retrieved.PublicId, upc.GetPublicId()) + // these asserts are super basic because the main check we want is that lookupDbCred succeeded break } assert.Fail("want UsernamePassword credential from library with credential type UsernamePassword") diff --git a/internal/credential/vault/testing.go b/internal/credential/vault/testing.go index bec523201..817492655 100644 --- a/internal/credential/vault/testing.go +++ b/internal/credential/vault/testing.go @@ -168,7 +168,7 @@ func TestCredentials(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, librar var credentials []*Credential for i := 0; i < count; i++ { - credential, err := newCredential(lib.GetPublicId(), sessionId, fmt.Sprintf("vault/credential/%d", i), token.GetTokenHmac(), 5*time.Minute) + credential, err := newCredential(lib.GetPublicId(), fmt.Sprintf("vault/credential/%d", i), token.GetTokenHmac(), 5*time.Minute) assert.NoError(err) require.NotNil(credential) @@ -177,7 +177,7 @@ func TestCredentials(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, librar require.NotNil(id) credential.PublicId = id - query, queryValues := credential.insertQuery() + query, queryValues := insertQuery(credential, sessionId) rows, err2 := rw.Exec(ctx, query, queryValues) assert.Equal(1, rows) assert.NoError(err2)