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)