diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb2cc1db7..ee2a2aecdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. ### Bug Fixes +* vault: Correctly handle Vault credential stores and libraries that are linked to an + expired Vault token. ([Issue](https://github.com/hashicorp/boundary/issues/2179), + [PR](https://github.com/hashicorp/boundary/pull/2399)). * aws host catalog: Fix an issue where the request to list hosts could timeout on a large number of hosts ([Issue](https://github.com/hashicorp/boundary/issues/2224), @@ -40,8 +43,8 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. used because different filters return values with different casing ([PR](https://github.com/hashicorp/boundary-plugin-host-azure/pull/8)) * sessions: Fix an issue where sessions could not have more than one connection - ([Issue](https://github.com/hashicorp/boundary/issues/2362)), - ([PR](https://github.com/hashicorp/boundary/pull/2369)) + ([Issue](https://github.com/hashicorp/boundary/issues/2362), + [PR](https://github.com/hashicorp/boundary/pull/2369)) * workers: Fix repeating error in logs when connected to HCP Boundary about an unimplemented HcpbWorkers call ([PR](https://github.com/hashicorp/boundary/pull/2361)) diff --git a/api/credentialstores/vault_credential_store_attributes.gen.go b/api/credentialstores/vault_credential_store_attributes.gen.go index d9871b9bec..e642356fb1 100644 --- a/api/credentialstores/vault_credential_store_attributes.gen.go +++ b/api/credentialstores/vault_credential_store_attributes.gen.go @@ -19,6 +19,7 @@ type VaultCredentialStoreAttributes struct { ClientCertificateKey string `json:"client_certificate_key,omitempty"` ClientCertificateKeyHmac string `json:"client_certificate_key_hmac,omitempty"` WorkerFilter string `json:"worker_filter,omitempty"` + TokenStatus string `json:"token_status,omitempty"` } func AttributesMapToVaultCredentialStoreAttributes(in map[string]interface{}) (*VaultCredentialStoreAttributes, error) { diff --git a/internal/cmd/commands/credentialstorescmd/funcs.go b/internal/cmd/commands/credentialstorescmd/funcs.go index 11b0e74c29..450066c0df 100644 --- a/internal/cmd/commands/credentialstorescmd/funcs.go +++ b/internal/cmd/commands/credentialstorescmd/funcs.go @@ -205,6 +205,7 @@ var keySubstMap = map[string]string{ "tls_server_name": "TLS Server Name", "tls_skip_verify": "Skip TLS Verification", "token_hmac": "Token HMAC", + "token_status": "Token Status", "client_certificate": "Client Certificate", "client_certificate_key_hmac": "Client Certificate Key HMAC", "worker_filter": "Worker Filter", diff --git a/internal/credential/vault/jobs.go b/internal/credential/vault/jobs.go index 384e22f8b0..7a035309e9 100644 --- a/internal/credential/vault/jobs.go +++ b/internal/credential/vault/jobs.go @@ -139,7 +139,7 @@ func (r *TokenRenewalJob) Run(ctx context.Context) error { return errors.Wrap(ctx, err, op) } - var ps []*privateStore + var ps []*renewRevokeStore // Fetch all tokens that will reach their renewal point within the renewalWindow. // This is done to avoid constantly scheduling the token renewal job when there are multiple tokens // set to renew in sequence. @@ -151,13 +151,14 @@ func (r *TokenRenewalJob) Run(ctx context.Context) error { // Set numProcessed and numTokens for status report r.numProcessed, r.numTokens = 0, len(ps) - for _, s := range ps { + for _, as := range ps { + s := as.Store // Verify context is not done before renewing next token if err := ctx.Err(); err != nil { return errors.Wrap(ctx, err, op) } if err := r.renewToken(ctx, s); err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error renewing token", "credential store id", s.StoreId, "token status", s.TokenStatus)) + event.WriteError(ctx, op, err, event.WithInfoMsg("error renewing token", "credential store id", s.PublicId, "token status", s.TokenStatus)) } r.numProcessed++ } @@ -165,7 +166,7 @@ func (r *TokenRenewalJob) Run(ctx context.Context) error { return nil } -func (r *TokenRenewalJob) renewToken(ctx context.Context, s *privateStore) error { +func (r *TokenRenewalJob) renewToken(ctx context.Context, s *clientStore) error { const op = "vault.(TokenRenewalJob).renewToken" databaseWrapper, err := r.kms.GetWrapper(ctx, s.ProjectId, kms.KeyPurposeDatabase) if err != nil { @@ -201,7 +202,7 @@ func (r *TokenRenewalJob) renewToken(ctx context.Context, s *privateStore) error return errors.New(ctx, errors.Unknown, op, "token expired but failed to update repo") } if s.TokenStatus == string(CurrentToken) { - event.WriteSysEvent(ctx, op, "Vault credential store current token has expired", "credential store id", s.StoreId) + event.WriteSysEvent(ctx, op, "Vault credential store current token has expired", "credential store id", s.PublicId) } // Set credentials associated with this token to expired as Vault will already cascade delete them @@ -373,7 +374,7 @@ or )) ` - var ps []*privateStore + var ps []*renewRevokeStore err := r.reader.SearchWhere(ctx, &ps, where, nil, db.WithLimit(r.limit)) if err != nil { return errors.Wrap(ctx, err, op) @@ -381,13 +382,14 @@ or // Set numProcessed and numTokens for s report r.numProcessed, r.numTokens = 0, len(ps) - for _, s := range ps { + for _, as := range ps { + s := as.Store // Verify context is not done before renewing next token if err := ctx.Err(); err != nil { return errors.Wrap(ctx, err, op) } if err := r.revokeToken(ctx, s); err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error revoking token", "credential store id", s.StoreId)) + event.WriteError(ctx, op, err, event.WithInfoMsg("error revoking token", "credential store id", s.PublicId)) } r.numProcessed++ } @@ -395,7 +397,7 @@ or return nil } -func (r *TokenRevocationJob) revokeToken(ctx context.Context, s *privateStore) error { +func (r *TokenRevocationJob) revokeToken(ctx context.Context, s *clientStore) error { const op = "vault.(TokenRevocationJob).revokeToken" databaseWrapper, err := r.kms.GetWrapper(ctx, s.ProjectId, kms.KeyPurposeDatabase) if err != nil { diff --git a/internal/credential/vault/jobs_test.go b/internal/credential/vault/jobs_test.go index d4af1d315e..9129705a8b 100644 --- a/internal/credential/vault/jobs_test.go +++ b/internal/credential/vault/jobs_test.go @@ -2031,7 +2031,7 @@ func TestCredentialStoreCleanupJob_Run(t *testing.T) { assert.Equal(1, r.numStores) // Lookup of cs1 and its token should fail - agg := allocPublicStore() + agg := allocListLookupStore() agg.PublicId = cs1.PublicId err = rw.LookupByPublicId(context.Background(), agg) require.Error(err) @@ -2076,7 +2076,7 @@ func TestCredentialStoreCleanupJob_Run(t *testing.T) { assert.Equal(1, r.numStores) // Lookup of cs2 and its token should fail - agg = allocPublicStore() + agg = allocListLookupStore() agg.PublicId = cs2.PublicId err = rw.LookupByPublicId(context.Background(), agg) require.Error(err) diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index f143381d36..700a299be1 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -24,7 +24,7 @@ var _ credential.Dynamic = (*baseCred)(nil) type baseCred struct { *Credential - lib *privateLibrary + lib *issueCredentialLibrary secretData map[string]interface{} } @@ -131,48 +131,48 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err }, nil } -var _ credential.Library = (*privateLibrary)(nil) +var _ credential.Library = (*issueCredentialLibrary)(nil) -// A privateLibrary contains all the values needed to connect to Vault and +// A issueCredentialLibrary contains all the values needed to connect to Vault and // retrieve credentials. -type privateLibrary struct { +type issueCredentialLibrary struct { PublicId string `gorm:"primary_key"` StoreId string - CredType string `gorm:"column:credential_type"` - UsernameAttribute string - PasswordAttribute string - PrivateKeyAttribute string - PrivateKeyPassphraseAttribute string Name string Description string CreateTime *timestamp.Timestamp UpdateTime *timestamp.Timestamp Version uint32 - ProjectId string 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 - TokenHmac []byte 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 *privateLibrary) clone() *privateLibrary { +func (pl *issueCredentialLibrary) clone() *issueCredentialLibrary { // The 'append(a[:0:0], a...)' comes from // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F - return &privateLibrary{ + return &issueCredentialLibrary{ PublicId: pl.PublicId, StoreId: pl.StoreId, CredType: pl.CredType, @@ -207,15 +207,15 @@ func (pl *privateLibrary) clone() *privateLibrary { } } -func (pl *privateLibrary) GetPublicId() string { return pl.PublicId } -func (pl *privateLibrary) GetStoreId() string { return pl.StoreId } -func (pl *privateLibrary) GetName() string { return pl.Name } -func (pl *privateLibrary) GetDescription() string { return pl.Description } -func (pl *privateLibrary) GetVersion() uint32 { return pl.Version } -func (pl *privateLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } -func (pl *privateLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } +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 *privateLibrary) CredentialType() credential.Type { +func (pl *issueCredentialLibrary) CredentialType() credential.Type { switch ct := pl.CredType; ct { case "": return credential.UnspecifiedType @@ -224,8 +224,8 @@ func (pl *privateLibrary) CredentialType() credential.Type { } } -func (pl *privateLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { - const op = "vault.(privateLibrary).decrypt" +func (pl *issueCredentialLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "vault.(issueCredentialLibrary).decrypt" if pl.CtToken != nil { type ptk struct { @@ -257,8 +257,8 @@ func (pl *privateLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) return nil } -func (pl *privateLibrary) client(ctx context.Context) (vaultClient, error) { - const op = "vault.(privateLibrary).client" +func (pl *issueCredentialLibrary) client(ctx context.Context) (vaultClient, error) { + const op = "vault.(issueCredentialLibrary).client" clientConfig := &clientConfig{ Addr: pl.VaultAddress, Token: pl.Token, @@ -289,7 +289,7 @@ type dynamicCred 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) { +func (pl *issueCredentialLibrary) 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() @@ -338,12 +338,12 @@ func (pl *privateLibrary) retrieveCredential(ctx context.Context, op errors.Op, } // TableName returns the table name for gorm. -func (pl *privateLibrary) TableName() string { - return "credential_vault_library_private" +func (pl *issueCredentialLibrary) TableName() string { + return "credential_vault_library_issue_credentials" } -func (r *Repository) getPrivateLibraries(ctx context.Context, requests []credential.Request) ([]*privateLibrary, error) { - const op = "vault.(Repository).getPrivateLibraries" +func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]*issueCredentialLibrary, error) { + const op = "vault.(Repository).getIssueCredLibraries" mapper, err := newMapper(requests) if err != nil { @@ -357,7 +357,7 @@ func (r *Repository) getPrivateLibraries(ctx context.Context, requests []credent } inClause := strings.Join(inClauseSpots, ",") - query := fmt.Sprintf(selectPrivateLibrariesQuery, inClause) + query := fmt.Sprintf(selectLibrariesQuery, inClause) var params []interface{} for idx, v := range libIds { @@ -369,9 +369,9 @@ func (r *Repository) getPrivateLibraries(ctx context.Context, requests []credent } defer rows.Close() - var libs []*privateLibrary + var libs []*issueCredentialLibrary for rows.Next() { - var lib privateLibrary + var lib issueCredentialLibrary if err := r.reader.ScanRows(ctx, rows, &lib); err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("scan row failed")) } diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index 6bd5453900..8e2bf1a310 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -264,7 +264,7 @@ func TestRepository_getPrivateLibraries(t *testing.T) { requests = append(requests, req) } - gotLibs, err := repo.getPrivateLibraries(ctx, requests) + gotLibs, err := repo.getIssueCredLibraries(ctx, requests) assert.NoError(err) require.NotNil(gotLibs) assert.Len(gotLibs, len(libs)) @@ -414,7 +414,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "library-not-username-password-type", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UnspecifiedType), }, }, @@ -423,7 +423,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-no-username-default-password-attribute", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -435,7 +435,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-no-password-default-username-attribute", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -447,7 +447,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-default-attributes", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -463,7 +463,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-override-attributes", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", PasswordAttribute: "test-password", @@ -483,7 +483,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-default-username-override-password", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), PasswordAttribute: "test-password", }, @@ -502,7 +502,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-override-username-default-password", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", }, @@ -521,7 +521,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-username-override", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "missing-username", }, @@ -537,7 +537,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-password-override", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "missing-password", }, @@ -553,7 +553,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-metadata-field", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -568,7 +568,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-data-field", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -580,7 +580,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-username-default-password-attribute", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -595,7 +595,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-passsword-default-username-attribute", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -610,7 +610,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -626,7 +626,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -639,7 +639,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-additional-field", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -656,7 +656,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-default-attributes", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]interface{}{ @@ -675,7 +675,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-override-attributes", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", PasswordAttribute: "test-password", @@ -698,7 +698,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-default-username-override-password", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), PasswordAttribute: "test-password", }, @@ -720,7 +720,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-override-username-default-password", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", }, @@ -779,7 +779,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "library-not-ssh-private-key-type", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.UnspecifiedType), }, }, @@ -788,7 +788,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-no-username-default-pk-attribute", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -800,7 +800,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-no-pk-default-username-attribute", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -812,7 +812,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-attributes", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -828,7 +828,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-attributes-with-passphrase", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -846,7 +846,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-attributes", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -866,7 +866,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-attributes-with-passphrase", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -890,7 +890,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-username-override-pk", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), PrivateKeyAttribute: "test-pk", }, @@ -909,7 +909,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-username-default-pk", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", }, @@ -928,7 +928,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-username-override", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "missing-username", }, @@ -944,7 +944,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-pk-override", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "missing-pk", }, @@ -960,7 +960,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-metadata-field", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -975,7 +975,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-data-field", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -987,7 +987,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-username-default-pk-attribute", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -1002,7 +1002,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-pk-default-username-attribute", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -1017,7 +1017,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -1033,7 +1033,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-invalid-data-type", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -1046,7 +1046,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-additional-field", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -1063,7 +1063,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-attributes", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -1082,7 +1082,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-attributes-with-passphrase", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]interface{}{ @@ -1103,7 +1103,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-attributes", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -1126,7 +1126,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-attributes-with-passphrase", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -1153,7 +1153,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-username-override-pk", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), PrivateKeyAttribute: "test-pk", }, @@ -1175,7 +1175,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-username-default-pk", given: &baseCred{ - lib: &privateLibrary{ + lib: &issueCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", }, diff --git a/internal/credential/vault/private_store.go b/internal/credential/vault/private_store.go index d89cd7d07a..311103dc68 100644 --- a/internal/credential/vault/private_store.go +++ b/internal/credential/vault/private_store.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" @@ -12,40 +11,13 @@ import ( "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" ) -func (r *Repository) listRevokePrivateStores(ctx context.Context, opt ...Option) ([]*privateStore, error) { - const op = "vault.(Repository).listRevokePrivateStores" - - opts := getOpts(opt...) - limit := r.defaultLimit - if opts.withLimit != 0 { - limit = opts.withLimit - } - - var stores []*privateStore - where, values := "token_status = ?", []interface{}{"revoke"} - if err := r.reader.SearchWhere(ctx, &stores, where, values, db.WithLimit(limit)); err != nil { - return nil, errors.Wrap(ctx, err, op) - } - - for _, store := range stores { - databaseWrapper, err := r.kms.GetWrapper(ctx, store.ProjectId, kms.KeyPurposeDatabase) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) - } - if err := store.decrypt(ctx, databaseWrapper); err != nil { - return nil, errors.Wrap(ctx, err, op) - } - } - return stores, nil -} - -func (r *Repository) lookupPrivateStore(ctx context.Context, publicId string) (*privateStore, error) { - const op = "vault.(Repository).lookupPrivateStore" +func (r *Repository) lookupClientStore(ctx context.Context, publicId string) (*clientStore, error) { + const op = "vault.(Repository).lookupClientStore" if publicId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") } - ps := allocPrivateStore() - if err := r.reader.LookupWhere(ctx, &ps, "public_id = ? and token_status = ?", []interface{}{publicId, CurrentToken}); err != nil { + ps := allocClientStore() + if err := r.reader.LookupWhere(ctx, &ps, "public_id = ?", []interface{}{publicId}); err != nil { if errors.IsNotFoundError(err) { return nil, nil } @@ -64,53 +36,40 @@ func (r *Repository) lookupPrivateStore(ctx context.Context, publicId string) (* return ps, nil } -type privateStore struct { - PublicId string `gorm:"primary_key"` - ProjectId string - Name string - Description string - CreateTime *timestamp.Timestamp - UpdateTime *timestamp.Timestamp - DeleteTime *timestamp.Timestamp - Version uint32 - VaultAddress string - Namespace string - CaCert []byte - TlsServerName string - TlsSkipVerify bool - WorkerFilter string - StoreId string - TokenHmac []byte - Token TokenSecret - CtToken []byte - TokenCreateTime *timestamp.Timestamp - TokenUpdateTime *timestamp.Timestamp - TokenLastRenewalTime *timestamp.Timestamp - TokenExpirationTime *timestamp.Timestamp - TokenRenewalTime *timestamp.Timestamp - TokenKeyId string - TokenStatus string - ClientCert []byte - ClientKeyId string - ClientKey KeySecret - CtClientKey []byte - ClientCertKeyHmac []byte +// clientStore is a Vault credential store that contains all the data needed to create +// a Vault client. If the Vault token for the store is expired all token data will be null +// other than the status of expired. +type clientStore struct { + PublicId string `gorm:"primary_key"` + ProjectId string + DeleteTime *timestamp.Timestamp + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + WorkerFilter string + TokenHmac []byte + Token TokenSecret + CtToken []byte + TokenRenewalTime *timestamp.Timestamp + TokenKeyId string + TokenStatus string + ClientCert []byte + ClientKeyId string + ClientKey KeySecret + CtClientKey []byte } -func allocPrivateStore() *privateStore { - return &privateStore{} +func allocClientStore() *clientStore { + return &clientStore{} } -func (ps *privateStore) toCredentialStore() *CredentialStore { +func (ps *clientStore) toCredentialStore() *CredentialStore { cs := allocCredentialStore() cs.PublicId = ps.PublicId cs.ProjectId = ps.ProjectId - cs.Name = ps.Name - cs.Description = ps.Description - cs.CreateTime = ps.CreateTime - cs.UpdateTime = ps.UpdateTime cs.DeleteTime = ps.DeleteTime - cs.Version = ps.Version cs.VaultAddress = ps.VaultAddress cs.Namespace = ps.Namespace cs.CaCert = ps.CaCert @@ -120,28 +79,22 @@ func (ps *privateStore) toCredentialStore() *CredentialStore { cs.privateToken = ps.token() if ps.ClientCert != nil { cert := allocClientCertificate() - cert.StoreId = ps.StoreId + cert.StoreId = ps.PublicId cert.Certificate = ps.ClientCert cert.CtCertificateKey = ps.CtClientKey - cert.CertificateKeyHmac = ps.ClientCertKeyHmac cert.KeyId = ps.ClientKeyId cs.privateClientCert = cert } return cs } -func (ps *privateStore) token() *Token { +func (ps *clientStore) token() *Token { if ps.TokenHmac != nil { tk := allocToken() - tk.StoreId = ps.StoreId tk.TokenHmac = ps.TokenHmac - tk.LastRenewalTime = ps.TokenLastRenewalTime - tk.ExpirationTime = ps.TokenExpirationTime - tk.CreateTime = ps.TokenCreateTime - tk.UpdateTime = ps.TokenUpdateTime + tk.Status = ps.TokenStatus tk.CtToken = ps.CtToken tk.KeyId = ps.TokenKeyId - tk.Status = ps.TokenStatus return tk } @@ -149,8 +102,8 @@ func (ps *privateStore) token() *Token { return nil } -func (ps *privateStore) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { - const op = "vault.(privateStore).decrypt" +func (ps *clientStore) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "vault.(clientStore).decrypt" if ps.CtToken != nil { type ptk struct { @@ -182,8 +135,8 @@ func (ps *privateStore) decrypt(ctx context.Context, cipher wrapping.Wrapper) er return nil } -func (ps *privateStore) client(ctx context.Context) (vaultClient, error) { - const op = "vault.(privateStore).client" +func (ps *clientStore) client(ctx context.Context) (vaultClient, error) { + const op = "vault.(clientStore).client" clientConfig := &clientConfig{ Addr: ps.VaultAddress, Token: ps.Token, @@ -206,9 +159,26 @@ func (ps *privateStore) client(ctx context.Context) (vaultClient, error) { } // GetPublicId returns the public id. -func (ps *privateStore) GetPublicId() string { return ps.PublicId } +func (ps *clientStore) GetPublicId() string { return ps.PublicId } + +// TableName returns the table name for gorm. +func (ps *clientStore) TableName() string { + return "credential_vault_store_client" +} + +// renewRevokeStore is a clientStore that is not limited to the current token and includes +// 'current', 'maintaining' and 'revoke' tokens. +type renewRevokeStore struct { + Store *clientStore `gorm:"embedded"` +} + +func allocRenewRevokeStore() *renewRevokeStore { + return &renewRevokeStore{ + Store: allocClientStore(), + } +} // TableName returns the table name for gorm. -func (ps *privateStore) TableName() string { - return "credential_vault_store_private" +func (ps *renewRevokeStore) TableName() string { + return "credential_vault_token_renewal_revocation" } diff --git a/internal/credential/vault/private_store_test.go b/internal/credential/vault/private_store_test.go index c6e8a95232..0c4581b602 100644 --- a/internal/credential/vault/private_store_test.go +++ b/internal/credential/vault/private_store_test.go @@ -77,7 +77,7 @@ func TestRepository_lookupPrivateStore(t *testing.T) { assert.NotNil(origLookup.Token()) assert.Equal(orig.GetPublicId(), origLookup.GetPublicId()) - got, err := repo.lookupPrivateStore(ctx, orig.GetPublicId()) + got, err := repo.lookupClientStore(ctx, orig.GetPublicId()) assert.NoError(err) require.NotNil(got) assert.Equal(orig.GetPublicId(), got.GetPublicId()) diff --git a/internal/credential/vault/query.go b/internal/credential/vault/query.go index 3346a71afe..dbbae22b7a 100644 --- a/internal/credential/vault/query.go +++ b/internal/credential/vault/query.go @@ -87,9 +87,9 @@ delete from credential_vault_client_certificate where store_id = ?; ` - selectPrivateLibrariesQuery = ` + selectLibrariesQuery = ` select * - from credential_vault_library_private + from credential_vault_library_issue_credentials where public_id in (%s); ` diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index d35ca547ad..7be4bcd0d5 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -302,7 +302,7 @@ func (r *Repository) UpdateCredentialLibrary(ctx context.Context, projectId stri return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) } - pl := allocPublicLibrary() + pl := allocListLookupLibrary() pl.PublicId = l.PublicId if err := rr.LookupByPublicId(ctx, pl); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve updated credential library")) @@ -330,7 +330,7 @@ func (r *Repository) LookupCredentialLibrary(ctx context.Context, publicId strin if publicId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") } - l := allocPublicLibrary() + l := allocListLookupLibrary() l.PublicId = publicId if err := r.reader.LookupByPublicId(ctx, l); err != nil { if errors.IsNotFoundError(err) { @@ -341,10 +341,10 @@ func (r *Repository) LookupCredentialLibrary(ctx context.Context, publicId strin return l.toCredentialLibrary(), nil } -// publicLibrary is a credential library and any of library's credential +// listLookupLibrary is a credential library and any of library's credential // mapping overrides. It does not include encrypted data and is safe to // return external to boundary. -type publicLibrary struct { +type listLookupLibrary struct { PublicId string `gorm:"primary_key"` StoreId string Name string @@ -362,11 +362,11 @@ type publicLibrary struct { PrivateKeyPassphraseAttribute string } -func allocPublicLibrary() *publicLibrary { - return &publicLibrary{} +func allocListLookupLibrary() *listLookupLibrary { + return &listLookupLibrary{} } -func (pl *publicLibrary) toCredentialLibrary() *CredentialLibrary { +func (pl *listLookupLibrary) toCredentialLibrary() *CredentialLibrary { cl := allocCredentialLibrary() cl.PublicId = pl.PublicId cl.StoreId = pl.StoreId @@ -405,10 +405,10 @@ func (pl *publicLibrary) toCredentialLibrary() *CredentialLibrary { } // TableName returns the table name for gorm. -func (_ *publicLibrary) TableName() string { return "credential_vault_library_public" } +func (_ *listLookupLibrary) TableName() string { return "credential_vault_library_list_lookup" } // GetPublicId returns the public id. -func (pl *publicLibrary) GetPublicId() string { return pl.PublicId } +func (pl *listLookupLibrary) GetPublicId() string { return pl.PublicId } // DeleteCredentialLibrary deletes publicId from the repository and returns // the number of records deleted. diff --git a/internal/credential/vault/repository_credential_library_test.go b/internal/credential/vault/repository_credential_library_test.go index 05d12482bb..5a0418f5c6 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -1621,6 +1621,47 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { assert.NoError(db.TestVerifyOplog(t, rw, got2.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) }) + t.Run("valid-update-with-expired-store-token", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1) + cs := css[0] + + in := &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-name-repo", + }, + } + + in.StoreId = cs.GetPublicId() + got, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), in) + assert.NoError(err) + require.NotNil(got) + + // Expire the credential store Vault token + rows, err := rw.Exec(context.Background(), + "update credential_vault_token set status = ? where token_hmac = ?", + []interface{}{ExpiredToken, cs.Token().TokenHmac}) + require.NoError(err) + require.Equal(1, rows) + + got.Name = "new-name" + updated, gotCount, err := repo.UpdateCredentialLibrary(ctx, prj.GetPublicId(), got, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(updated) + assert.Equal("new-name", updated.Name) + assert.Equal(1, gotCount) + }) + t.Run("change-project-id", func(t *testing.T) { assert, require := assert.New(t), require.New(t) ctx := context.Background() @@ -1662,7 +1703,15 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) { { _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + + cs := css[0] + csWithExpiredToken := css[1] + rows, err := rw.Exec(context.Background(), + "update credential_vault_token set status = ? where token_hmac = ?", + []interface{}{ExpiredToken, csWithExpiredToken.Token().TokenHmac}) + require.NoError(t, err) + require.Equal(t, 1, rows) tests := []struct { name string @@ -1678,6 +1727,17 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) { }, }, }, + + { + name: "valid-with-expired-cred-store-token", + in: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: csWithExpiredToken.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + }, + }, + }, { name: "valid-username-password-credential-type", in: &CredentialLibrary{ diff --git a/internal/credential/vault/repository_credential_store.go b/internal/credential/vault/repository_credential_store.go index 1b58f058e4..1c132b9d31 100644 --- a/internal/credential/vault/repository_credential_store.go +++ b/internal/credential/vault/repository_credential_store.go @@ -249,7 +249,7 @@ func (r *Repository) LookupCredentialStore(ctx context.Context, publicId string, if publicId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") } - agg := allocPublicStore() + agg := allocListLookupStore() agg.PublicId = publicId if err := r.reader.LookupByPublicId(ctx, agg); err != nil { if errors.IsNotFoundError(err) { @@ -260,34 +260,31 @@ func (r *Repository) LookupCredentialStore(ctx context.Context, publicId string, return agg.toCredentialStore(), nil } -type publicStore struct { - PublicId string `gorm:"primary_key"` - ProjectId string - Name string - Description string - CreateTime *timestamp.Timestamp - UpdateTime *timestamp.Timestamp - Version uint32 - VaultAddress string - Namespace string - CaCert []byte - TlsServerName string - TlsSkipVerify bool - WorkerFilter string - TokenHmac []byte - TokenCreateTime *timestamp.Timestamp - TokenUpdateTime *timestamp.Timestamp - TokenLastRenewalTime *timestamp.Timestamp - TokenExpirationTime *timestamp.Timestamp - ClientCert []byte - ClientCertKeyHmac []byte +type listLookupStore struct { + PublicId string `gorm:"primary_key"` + ProjectId string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + WorkerFilter string + TokenHmac []byte + TokenStatus string + ClientCert []byte + ClientCertKeyHmac []byte } -func allocPublicStore() *publicStore { - return &publicStore{} +func allocListLookupStore() *listLookupStore { + return &listLookupStore{} } -func (ps *publicStore) toCredentialStore() *CredentialStore { +func (ps *listLookupStore) toCredentialStore() *CredentialStore { cs := allocCredentialStore() cs.PublicId = ps.PublicId cs.ProjectId = ps.ProjectId @@ -303,15 +300,10 @@ func (ps *publicStore) toCredentialStore() *CredentialStore { cs.TlsSkipVerify = ps.TlsSkipVerify cs.WorkerFilter = ps.WorkerFilter - if ps.TokenHmac != nil { - tk := allocToken() - tk.TokenHmac = ps.TokenHmac - tk.LastRenewalTime = ps.TokenLastRenewalTime - tk.ExpirationTime = ps.TokenExpirationTime - tk.CreateTime = ps.TokenCreateTime - tk.UpdateTime = ps.TokenUpdateTime - cs.outputToken = tk - } + tk := allocToken() + tk.TokenHmac = ps.TokenHmac + tk.Status = ps.TokenStatus + cs.outputToken = tk if ps.ClientCert != nil { cert := allocClientCertificate() @@ -323,10 +315,10 @@ func (ps *publicStore) toCredentialStore() *CredentialStore { } // TableName returns the table name for gorm. -func (_ *publicStore) TableName() string { return "credential_vault_store_public" } +func (_ *listLookupStore) TableName() string { return "credential_vault_store_list_lookup" } // GetPublicId returns the public id. -func (ps *publicStore) GetPublicId() string { return ps.PublicId } +func (ps *listLookupStore) GetPublicId() string { return ps.PublicId } // UpdateCredentialStore updates the repository entry for cs.PublicId with // the values in cs for the fields listed in fieldMaskPaths. It returns a @@ -449,7 +441,7 @@ func (r *Repository) UpdateCredentialStore(ctx context.Context, cs *CredentialSt errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) } - ps, err := r.lookupPrivateStore(ctx, cs.GetPublicId()) + ps, err := r.lookupClientStore(ctx, cs.GetPublicId()) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup private credential store")) } @@ -613,7 +605,7 @@ func (r *Repository) UpdateCredentialStore(ctx context.Context, cs *CredentialSt } publicId := cs.PublicId - agg := allocPublicStore() + agg := allocListLookupStore() agg.PublicId = publicId if err := reader.LookupByPublicId(ctx, agg); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to lookup credential store: %s", publicId))) @@ -655,7 +647,7 @@ func (r *Repository) ListCredentialStores(ctx context.Context, projectIds []stri // non-zero signals an override of the default limit for the repo. limit = opts.withLimit } - var credentialStores []*publicStore + var credentialStores []*listLookupStore err := r.reader.SearchWhere(ctx, &credentialStores, "project_id in (?)", []interface{}{projectIds}, db.WithLimit(limit)) if err != nil { return nil, errors.Wrap(ctx, err, op) diff --git a/internal/credential/vault/repository_credential_store_test.go b/internal/credential/vault/repository_credential_store_test.go index 548b8dbb15..824c9227a5 100644 --- a/internal/credential/vault/repository_credential_store_test.go +++ b/internal/credential/vault/repository_credential_store_test.go @@ -235,9 +235,10 @@ func TestRepository_LookupCredentialStore(t *testing.T) { sche := scheduler.TestScheduler(t, conn, wrapper) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - stores := TestCredentialStores(t, conn, wrapper, prj.PublicId, 2) + stores := TestCredentialStores(t, conn, wrapper, prj.PublicId, 3) csWithClientCert := stores[0] csWithoutClientCert := stores[1] + csWithExpiredToken := stores[2] ccert := allocClientCertificate() ccert.StoreId = csWithoutClientCert.GetPublicId() @@ -245,6 +246,12 @@ func TestRepository_LookupCredentialStore(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, rows) + rows, err = rw.Exec(context.Background(), + "update credential_vault_token set status = ? where token_hmac = ?", + []interface{}{ExpiredToken, csWithExpiredToken.Token().TokenHmac}) + require.NoError(t, err) + require.Equal(t, 1, rows) + badId, err := newCredentialStoreId() assert.NoError(t, err) require.NotNil(t, badId) @@ -262,6 +269,12 @@ func TestRepository_LookupCredentialStore(t *testing.T) { want: csWithClientCert, wantClientCert: true, }, + { + name: "valid-with-expired-token", + id: csWithExpiredToken.GetPublicId(), + want: csWithExpiredToken, + wantClientCert: true, + }, { name: "valid-without-client-cert", id: csWithoutClientCert.GetPublicId(), @@ -1026,6 +1039,7 @@ func TestRepository_UpdateCredentialStore_VaultToken(t *testing.T) { name string newTokenOpts []TestOption wantOldTokenStatus TokenStatus + updateToken func(ctx context.Context, tokenHmac []byte) wantCount int wantErr errors.Code }{ @@ -1034,6 +1048,17 @@ func TestRepository_UpdateCredentialStore_VaultToken(t *testing.T) { wantOldTokenStatus: MaintainingToken, wantCount: 1, }, + { + name: "valid-token-expired", + wantOldTokenStatus: ExpiredToken, + updateToken: func(ctx context.Context, tokenHmac []byte) { + _, err := rw.Exec(ctx, + "update credential_vault_token set status = ? where token_hmac = ?", + []interface{}{ExpiredToken, tokenHmac}) + require.NoError(t, err) + }, + wantCount: 1, + }, { name: "token-missing-capabilities", newTokenOpts: []TestOption{WithPolicies([]string{"default"})}, @@ -1080,6 +1105,10 @@ func TestRepository_UpdateCredentialStore_VaultToken(t *testing.T) { assert.NoError(err) require.NotNil(orig) + if tt.updateToken != nil { + tt.updateToken(ctx, orig.outputToken.TokenHmac) + } + // update _, updateToken := v.CreateToken(t, tt.newTokenOpts...) @@ -1143,12 +1172,12 @@ func TestRepository_UpdateCredentialStore_ClientCert(t *testing.T) { return nil } - assertUpdated := func(t *testing.T, org, updated *ClientCertificate, ps *privateStore) { + assertUpdated := func(t *testing.T, org, updated *ClientCertificate, ps *clientStore) { assert.Equal(t, updated.Certificate, ps.ClientCert, "updated certificate") assert.Equal(t, updated.CertificateKey, []byte(ps.ClientKey), "updated certificate key") } - assertDeleted := func(t *testing.T, org, updated *ClientCertificate, ps *privateStore) { + assertDeleted := func(t *testing.T, org, updated *ClientCertificate, ps *clientStore) { assert.Nil(t, updated, "updated certificate") assert.Nil(t, ps.ClientCert, "ps ClientCert") assert.Nil(t, ps.ClientKey, "ps ClientKey") @@ -1160,7 +1189,7 @@ func TestRepository_UpdateCredentialStore_ClientCert(t *testing.T) { tls TestVaultTLS origFn func(t *testing.T, v *TestVaultServer) *ClientCertificate updateFn func(t *testing.T, v *TestVaultServer) *ClientCertificate - wantFn func(t *testing.T, org, updated *ClientCertificate, ps *privateStore) + wantFn func(t *testing.T, org, updated *ClientCertificate, ps *clientStore) wantCount int wantErr errors.Code }{ @@ -1245,7 +1274,7 @@ func TestRepository_UpdateCredentialStore_ClientCert(t *testing.T) { require.NoError(err) assert.NotNil(got) - ps, err := repo.lookupPrivateStore(ctx, orig.GetPublicId()) + ps, err := repo.lookupClientStore(ctx, orig.GetPublicId()) require.NoError(err) tt.wantFn(t, origClientCert, updateClientCert, ps) }) @@ -1277,6 +1306,20 @@ func TestRepository_ListCredentialStores_Multiple_Scopes(t *testing.T) { total += numPerScope } + // Add some credential stores with expired tokens + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + prjs = append(prjs, prj.GetPublicId()) + + stores := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), numPerScope) + for _, cs := range stores { + rows, err := rw.Exec(context.Background(), + "update credential_vault_token set status = ? where token_hmac = ?", + []interface{}{ExpiredToken, cs.Token().TokenHmac}) + require.NoError(err) + require.Equal(1, rows) + } + total += numPerScope + got, err := repo.ListCredentialStores(context.Background(), prjs) require.NoError(err) assert.Equal(total, len(got)) @@ -1538,10 +1581,14 @@ group by store_id, status; assert.Len(libs, len(actualLibs)) } + // verify no revoke stores exist { - privStores, err := repo.listRevokePrivateStores(ctx) - assert.NoError(err) - assert.Empty(privStores) + rows, err := repo.reader.Query(ctx, + "select * from credential_vault_token_renewal_revocation where token_status = $1", + []interface{}{ExpiredToken}) + require.NoError(err) + defer rows.Close() + assert.False(rows.Next()) } // verify updating the credential store works @@ -1609,17 +1656,27 @@ group by store_id, status; } var deleteTime *timestamp.Timestamp - // still in privateStore delete time set + // still in clientStore delete time set { - privStores, err := repo.listRevokePrivateStores(ctx) - assert.NoError(err) - var privateStore *privateStore + rows, err := repo.reader.Query(ctx, + "select * from credential_vault_token_renewal_revocation where token_status = $1", + []interface{}{RevokeToken}) + require.NoError(err) + defer rows.Close() + + var privateStore *clientStore var storeIds []string - for _, v := range privStores { - id := v.GetPublicId() + for rows.Next() { + var s clientStore + err = repo.reader.ScanRows(ctx, rows, &s) + require.NoError(err) + require.NotNil(s) + + id := s.GetPublicId() storeIds = append(storeIds, id) if id == storeId { - privateStore = v + privateStore = &s + break } } assert.Contains(storeIds, storeId) @@ -1646,17 +1703,27 @@ group by store_id, status; assert.Equal(0, deleteCount) } - // still in privateStore delete time should not change + // still in clientStore delete time should not change { - privStores, err := repo.listRevokePrivateStores(ctx) - assert.NoError(err) - var privateStore *privateStore + rows, err := repo.reader.Query(ctx, + "select * from credential_vault_token_renewal_revocation where token_status = $1", + []interface{}{RevokeToken}) + require.NoError(err) + defer rows.Close() + + var privateStore *clientStore var storeIds []string - for _, v := range privStores { - id := v.GetPublicId() + for rows.Next() { + var s clientStore + err = repo.reader.ScanRows(ctx, rows, &s) + require.NoError(err) + require.NotNil(s) + + id := s.GetPublicId() storeIds = append(storeIds, id) if id == storeId { - privateStore = v + privateStore = &s + break } } assert.Contains(storeIds, storeId) diff --git a/internal/credential/vault/repository_credentials.go b/internal/credential/vault/repository_credentials.go index 575d1d6625..abb4d6a9b9 100644 --- a/internal/credential/vault/repository_credentials.go +++ b/internal/credential/vault/repository_credentials.go @@ -22,7 +22,7 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre return nil, errors.New(ctx, errors.InvalidParameter, op, "no requests") } - libs, err := r.getPrivateLibraries(ctx, requests) + libs, err := r.getIssueCredLibraries(ctx, requests) if err != nil { return nil, errors.Wrap(ctx, err, op) } diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 66237adea4..44b3c6849a 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -64,6 +64,22 @@ func TestRepository_IssueCredentials(t *testing.T) { assert.NoError(t, err) require.NotNil(t, origStore) + _, expToken := v.CreateToken(t, vault.WithPolicies([]string{"default", "boundary-controller", "database", "pki", "secret"})) + expCredStoreIn, err := vault.NewCredentialStore(prj.GetPublicId(), v.Addr, []byte(expToken), opts...) + assert.NoError(t, err) + require.NotNil(t, expCredStoreIn) + expStore, err := repo.CreateCredentialStore(ctx, expCredStoreIn) + assert.NoError(t, err) + require.NotNil(t, expStore) + + // Set previous token to expired in the database and revoke in Vault to validate a + // credential store with an expired token is correctly returned over the API + num, err := rw.Exec(context.Background(), "update credential_vault_token set status = ? where store_id = ?", + []interface{}{vault.ExpiredToken, expStore.PublicId}) + require.NoError(t, err) + assert.Equal(t, 1, num) + v.RevokeToken(t, expToken) + type libT int const ( libDB libT = iota @@ -75,6 +91,7 @@ func TestRepository_IssueCredentials(t *testing.T) { libErrKV libUsrPassKV libSshPkKV + libExpiredToken ) libs := make(map[libT]string) @@ -185,6 +202,19 @@ func TestRepository_IssueCredentials(t *testing.T) { require.NotNil(t, lib) libs[libSshPkKV] = lib.GetPublicId() } + { + libPath := path.Join("secret", "data", "my-up-secret") + opts := []vault.Option{ + vault.WithCredentialType(credential.UsernamePasswordType), + } + libIn, err := vault.NewCredentialLibrary(expStore.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[libExpiredToken] = lib.GetPublicId() + } at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) uId := at.GetIamUserId() @@ -327,6 +357,17 @@ func TestRepository_IssueCredentials(t *testing.T) { }, }, }, + { + name: "invalid-expired-token", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libExpiredToken], + Purpose: credential.BrokeredPurpose, + }, + }, + wantErr: errors.InvalidParameter, + }, } for _, tt := range tests { tt := tt diff --git a/internal/credential/vault/testing.go b/internal/credential/vault/testing.go index 33408b3dca..688842b0fa 100644 --- a/internal/credential/vault/testing.go +++ b/internal/credential/vault/testing.go @@ -689,6 +689,17 @@ func (v *TestVaultServer) CreateToken(t testing.TB, opt ...TestOption) (*vault.S return secret, token } +// RevokeToken calls /auth/token/revoke-self on v for the token. See +// https://www.vaultproject.io/api-docs/auth/token#revoke-a-token-self. +func (v *TestVaultServer) RevokeToken(t testing.TB, token string) { + t.Helper() + require := require.New(t) + vc := v.client(t).cl + vc.SetToken(token) + err := vc.Auth().Token().RevokeSelf("") + require.NoError(err) +} + // LookupToken calls /auth/token/lookup on v for the token. See // https://www.vaultproject.io/api-docs/auth/token#lookup-a-token. func (v *TestVaultServer) LookupToken(t testing.TB, token string) *vault.Secret { diff --git a/internal/credential/vault/testing_test.go b/internal/credential/vault/testing_test.go index 6b10c9d6c0..ed6aa83f6b 100644 --- a/internal/credential/vault/testing_test.go +++ b/internal/credential/vault/testing_test.go @@ -502,20 +502,29 @@ func TestTestVaultServer_LookupLease(t *testing.T) { func TestTestVaultServer_VerifyTokenInvalid(t *testing.T) { t.Parallel() - ctx := context.Background() - require := require.New(t) v := NewTestVaultServer(t, WithDockerNetwork(true)) _, token := v.CreateToken(t) - client := v.ClientUsingToken(t, token) - err := client.revokeToken(ctx) - require.NoError(err) + v.RevokeToken(t, token) v.VerifyTokenInvalid(t, token) // Verify fake token is not valid v.VerifyTokenInvalid(t, "fake-token") } +func TestTestVaultServer_RevokeToken(t *testing.T) { + t.Parallel() + v := NewTestVaultServer(t, WithDockerNetwork(true)) + + _, token := v.CreateToken(t) + + // Validate we can lookup the token + v.LookupToken(t, token) + + v.RevokeToken(t, token) + v.VerifyTokenInvalid(t, token) +} + func Test_testClientCert(t *testing.T) { assert, require := assert.New(t), require.New(t) diff --git a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go index 9052ea1176..d97e8c3494 100644 --- a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go +++ b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go @@ -675,6 +675,7 @@ func toProto(ctx context.Context, in credential.Store, opt ...handlers.Option) ( } if vaultIn.Token() != nil { attrs.TokenHmac = base64.RawURLEncoding.EncodeToString(vaultIn.Token().GetTokenHmac()) + attrs.TokenStatus = vaultIn.Token().GetStatus() } if vaultIn.GetWorkerFilter() != "" { if vaultWorkerFilterToProto { diff --git a/internal/daemon/controller/handlers/credentialstores/credentialstore_service_test.go b/internal/daemon/controller/handlers/credentialstores/credentialstore_service_test.go index 8b8a15eba8..5192438222 100644 --- a/internal/daemon/controller/handlers/credentialstores/credentialstore_service_test.go +++ b/internal/daemon/controller/handlers/credentialstores/credentialstore_service_test.go @@ -96,6 +96,7 @@ func TestList(t *testing.T) { VaultCredentialStoreAttributes: &pb.VaultCredentialStoreAttributes{ Address: wrapperspb.String(s.GetVaultAddress()), TokenHmac: base64.RawURLEncoding.EncodeToString(s.Token().GetTokenHmac()), + TokenStatus: s.Token().GetStatus(), ClientCertificate: wrapperspb.String(string(s.ClientCertificate().GetCertificate())), ClientCertificateKeyHmac: base64.RawURLEncoding.EncodeToString(s.ClientCertificate().GetCertificateKeyHmac()), // TODO: Add all fields including tls related fields, namespace, etc... @@ -471,6 +472,7 @@ func TestCreateVault(t *testing.T) { CaCert: wrapperspb.String(string(v.CaCert)), Address: wrapperspb.String(v.Addr), TokenHmac: "", + TokenStatus: "current", ClientCertificate: wrapperspb.String(string(v.ClientCert)), ClientCertificateKeyHmac: "", }, @@ -512,6 +514,7 @@ func TestCreateVault(t *testing.T) { CaCert: wrapperspb.String(string(v.CaCert)), Address: wrapperspb.String(v.Addr), TokenHmac: "", + TokenStatus: "current", ClientCertificate: wrapperspb.String(string(v.ClientCert)), ClientCertificateKeyHmac: "", }, @@ -795,6 +798,7 @@ func TestGet(t *testing.T) { VaultCredentialStoreAttributes: &pb.VaultCredentialStoreAttributes{ Address: wrapperspb.String(vaultStore.GetVaultAddress()), TokenHmac: base64.RawURLEncoding.EncodeToString(vaultStore.Token().GetTokenHmac()), + TokenStatus: vaultStore.Token().GetStatus(), ClientCertificate: wrapperspb.String(string(vaultStore.ClientCertificate().GetCertificate())), ClientCertificateKeyHmac: base64.RawURLEncoding.EncodeToString(vaultStore.ClientCertificate().GetCertificateKeyHmac()), }, @@ -1068,6 +1072,7 @@ func TestUpdateVault(t *testing.T) { res: func(in *pb.CredentialStore) *pb.CredentialStore { out := proto.Clone(in).(*pb.CredentialStore) out.GetVaultCredentialStoreAttributes().TokenHmac = "" + out.GetVaultCredentialStoreAttributes().TokenStatus = "current" return out }, }, diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index e66357e9d2..21434c70fa 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -3257,6 +3257,17 @@ func TestAuthorizeSession_Errors(t *testing.T) { sec, tok := v.CreateToken(t, vault.WithPolicies([]string{"default", "database"})) store := vault.TestCredentialStore(t, conn, wrapper, proj.GetPublicId(), v.Addr, tok, sec.Auth.Accessor) + sec1, tok1 := v.CreateToken(t, vault.WithPolicies([]string{"default", "database"})) + expiredStore := vault.TestCredentialStore(t, conn, wrapper, proj.GetPublicId(), v.Addr, tok1, sec1.Auth.Accessor) + + // Set previous token to expired in the database and revoke in Vault to validate a + // credential store with an expired token is correctly returned over the API + num, err := rw.Exec(context.Background(), "update credential_vault_token set status = ? where store_id = ?", + []interface{}{vault.ExpiredToken, expiredStore.PublicId}) + require.NoError(t, err) + assert.Equal(t, 1, num) + v.RevokeToken(t, tok1) + workerExists := func(tar target.Target) (version uint32) { server.TestKmsWorker(t, conn, wrapper) return tar.GetVersion() @@ -3356,6 +3367,30 @@ func TestAuthorizeSession_Errors(t *testing.T) { return tr.GetItem().GetVersion() } + expiredTokenLibrary := func(tar target.Target) (version uint32) { + credService, err := credentiallibraries.NewService(vaultCredRepoFn, iamRepoFn) + require.NoError(t, err) + clsResp, err := credService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ + CredentialStoreId: expiredStore.GetPublicId(), + Description: wrapperspb.String(fmt.Sprintf("Library Description for target %q", tar.GetName())), + Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(path.Join("database", "creds", "opened")), + }, + }, + }}) + require.NoError(t, err) + + tr, err := s.AddTargetCredentialSources(ctx, + &pbs.AddTargetCredentialSourcesRequest{ + Id: tar.GetPublicId(), + BrokeredCredentialSourceIds: []string{clsResp.GetItem().GetId()}, + Version: tar.GetVersion(), + }) + require.NoError(t, err) + return tr.GetItem().GetVersion() + } + cases := []struct { name string setup []func(target.Target) uint32 @@ -3386,6 +3421,11 @@ func TestAuthorizeSession_Errors(t *testing.T) { setup: []func(tcpTarget target.Target) uint32{workerExists, hostExists, misConfiguredlibraryExists}, err: true, }, + { + name: "expired token library", + setup: []func(tcpTarget target.Target) uint32{workerExists, hostExists, expiredTokenLibrary}, + err: true, + }, } for i, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/db/schema/migrations/oss/postgres/44/01_credentials.up.sql b/internal/db/schema/migrations/oss/postgres/44/01_credentials.up.sql index 4e0ebd098e..e8a4a47bb5 100644 --- a/internal/db/schema/migrations/oss/postgres/44/01_credentials.up.sql +++ b/internal/db/schema/migrations/oss/postgres/44/01_credentials.up.sql @@ -45,7 +45,7 @@ begin; drop view credential_vault_store_private; drop view credential_vault_credential_private; - -- Replaces view from 41/01_worker_filter_vault_cred_store.up.sql + -- Replaced in 49/01_vault_credentials.up.sql create view credential_vault_store_private as with active_tokens as ( @@ -101,7 +101,7 @@ begin; 'The view returns a separate row for each current, maintaining and revoke token; maintaining tokens should only be used for token/credential renewal and revocation. ' 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; - -- Replaces view from 41/01_worker_filter_vault_cred_store.up.sql + -- Replaced in 49/01_vault_credentials.up.sql create view credential_vault_store_public as select public_id, project_id, @@ -130,7 +130,7 @@ begin; 'credential_vault_store_public is a view where each row contains a credential store. ' 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; - -- Replaces view from 42/01_ssh_private_key_passphrase.up.sql + -- Replaced in 49/01_vault_credentials.up.sql create view credential_vault_library_private as with password_override (library_id, username_attribute, password_attribute) as ( @@ -188,7 +188,7 @@ begin; 'credential_vault_library_private is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; - -- Replaces view from 42/01_ssh_private_key_passphrase.up.sql + -- Replaced in 49/01_vault_credentials.up.sql create view credential_vault_library_public as select public_id, store_id, diff --git a/internal/db/schema/migrations/oss/postgres/49/01_vault_credentials.up.sql b/internal/db/schema/migrations/oss/postgres/49/01_vault_credentials.up.sql new file mode 100644 index 0000000000..e972054ecd --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/49/01_vault_credentials.up.sql @@ -0,0 +1,205 @@ +begin; + + -- update views + drop view credential_vault_store_public; + drop view credential_vault_library_public; + drop view credential_vault_library_private; + drop view credential_vault_store_private; + + create view credential_vault_token_renewal_revocation as + with + tokens as ( + select token, -- encrypted + token_hmac, + store_id, + -- renewal time is the midpoint between the last renewal time and the expiration time + last_renewal_time + (expiration_time - last_renewal_time) / 2 as renewal_time, + key_id, + status + from credential_vault_token + where status in ('current', 'maintaining', 'revoke') + ) + select store.public_id as public_id, + store.project_id as project_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.worker_filter as worker_filter, + store.delete_time as delete_time, + token.token as ct_token, -- encrypted + token.token_hmac as token_hmac, + token.renewal_time as token_renewal_time, + token.key_id as token_key_id, + token.status as token_status, + cert.certificate as client_cert, + cert.certificate_key as ct_client_key, -- encrypted + cert.key_id as client_key_id + from credential_vault_store store + join tokens token + on store.public_id = token.store_id + left join credential_vault_client_certificate cert + on store.public_id = cert.store_id; + comment on view credential_vault_token_renewal_revocation is + 'credential_vault_token_renewal_revocation is a view where each row contains a credential store and the credential store''s data needed to connect to Vault. ' + 'The view returns a separate row for each active token in Vault (current, maintaining and revoke tokens); this view should only be used for token renewal and revocation. ' + 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + + create view credential_vault_store_list_lookup as + select store.public_id as public_id, + store.project_id as project_id, + store.name as name, + store.description as description, + store.create_time as create_time, + store.update_time as update_time, + store.delete_time as delete_time, + store.version as version, + 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.worker_filter as worker_filter, + token.token_hmac as token_hmac, + coalesce(token.status, 'expired') as token_status, + cert.certificate as client_cert, + cert.certificate_key_hmac as client_cert_key_hmac + from credential_vault_store store + left join credential_vault_token token + on store.public_id = token.store_id + and token.status = 'current' + left join credential_vault_client_certificate cert + on store.public_id = cert.store_id + where store.delete_time is null; + comment on view credential_vault_store_list_lookup is + 'credential_vault_store_list_lookup is a view where each row contains a credential store. ' + 'If the Vault token has expired this view will return an empty token_hmac and a token_status of ''expired'' ' + 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; + + create view credential_vault_store_client as + select store.public_id as public_id, + store.project_id as project_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.worker_filter as worker_filter, + token.token as ct_token, -- encrypted + token.token_hmac as token_hmac, + coalesce(token.status, 'expired') as token_status, + token.key_id as token_key_id, + cert.certificate as client_cert, + cert.certificate_key as ct_client_key, -- encrypted + cert.key_id as client_key_id + from credential_vault_store store + left join credential_vault_token token + on store.public_id = token.store_id + and token.status = 'current' + left join credential_vault_client_certificate cert + on store.public_id = cert.store_id + where store.delete_time is null; + comment on view credential_vault_store_client is + 'credential_vault_store_client is a view where each row contains a credential store and the credential store''s data needed to connect to Vault. ' + 'The view returns the current token for the store, if the Vault token has expired this view will return an empty token_hmac and a token_status of ''expired'' ' + 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + + create view credential_vault_library_issue_credentials 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_username_password_mapping_override + ), + ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(private_key_attribute, wt_to_sentinel('no override')), + nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) + from credential_vault_library_ssh_private_key_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.project_id as project_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.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + 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, + coalesce(upasso.username_attribute,sshpk.username_attribute) + as username_attribute, + upasso.password_attribute as password_attribute, + sshpk.private_key_attribute as private_key_attribute, + sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute + from credential_vault_library library + join credential_vault_store_client store + on library.store_id = store.public_id + left join password_override upasso + on library.public_id = upasso.library_id + left join ssh_private_key_override sshpk + on library.public_id = sshpk.library_id; + comment on view credential_vault_library_issue_credentials is + 'credential_vault_library_issue_credentials is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' + 'This view should only be used when issuing credentials from a Vault credential library. Each row may contain encrypted data. ' + 'This view should not be used to retrieve data which will be returned external to boundary.'; + + create view credential_vault_library_list_lookup 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_username_password_mapping_override + ), + ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(private_key_attribute, wt_to_sentinel('no override')), + nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) + from credential_vault_library_ssh_private_key_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, + coalesce(upasso.username_attribute,sshpk.username_attribute) + as username_attribute, + upasso.password_attribute as password_attribute, + sshpk.private_key_attribute as private_key_attribute, + sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute + from credential_vault_library library + left join password_override upasso + on library.public_id = upasso.library_id + left join ssh_private_key_override sshpk + on library.public_id = sshpk.library_id; + comment on view credential_vault_library_list_lookup is + 'credential_vault_library_list_lookup is a view where each row contains a credential library and any of library''s credential mapping overrides. ' + 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; + +commit; diff --git a/internal/db/sqltest/initdb.d/01_colors_persona.sql b/internal/db/sqltest/initdb.d/01_colors_persona.sql index da891a7974..489adfd2f3 100644 --- a/internal/db/sqltest/initdb.d/01_colors_persona.sql +++ b/internal/db/sqltest/initdb.d/01_colors_persona.sql @@ -224,16 +224,17 @@ begin; ('p____rcolors', 't_________cr', 's___1cr-sths'), ('p____rcolors', 't_________cr', 's___2cr-sths'); - insert into credential_vault_store - (project_id, public_id, name, description, vault_address, namespace) + (project_id, public_id, name, description, vault_address, namespace) values - ('p____bcolors', 'vs_______cvs', 'color vault store', 'None', 'https://vault.color', 'blue'); + ('p____bcolors', 'vs_______cvs1', 'color vault store 1', 'None', 'https://vault.color', 'blue'), + ('p____bcolors', 'vs_______cvs2', 'color vault store 2', 'Some', 'https://vault.color', 'blue'), + ('p____bcolors', 'vs_______cvs3', 'color vault store 3', 'Maybe', 'https://vault.color', 'blue'); insert into credential_vault_library - (store_id, public_id, name, description, vault_path, http_method) + (store_id, public_id, name, description, vault_path, http_method) values - ('vs_______cvs', 'vl______cvl', 'color vault library', 'None', '/secrets', 'GET'); + ('vs_______cvs1', 'vl______cvl', 'color vault library', 'None', '/secrets', 'GET'); insert into target_credential_library (project_id, target_id, credential_library_id, credential_purpose) diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_issue_credentials.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_issue_credentials.sql new file mode 100644 index 0000000000..d9cb1466b1 --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_issue_credentials.sql @@ -0,0 +1,99 @@ +-- credential_vault_library_issue_credentials tests the credential_vault_library_issue_credentials view + +begin; + + select plan(8); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate the setup data + select has_view('credential_vault_library_issue_credentials', 'view for issuing credentials does not exist'); + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______wvs'; + + select is(count(*), 4::bigint) + from credential_vault_library_ssh_private_key_mapping_override + where library_id in ('vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); + + select is(count(*), 4::bigint) + from credential_vault_library_username_password_mapping_override + where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); + + select is(count(*), 8::bigint) + from credential_vault_library_mapping_override + where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7', 'vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); + + -- create test vault tokens + insert into credential_vault_token + (token_hmac, token, store_id, last_renewal_time, expiration_time, key_id, status) + values + ('cvs_token2', 'token', 'vs_______wvs', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'maintaining'), + ('cvs_token1', 'token', 'vs_______wvs', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token3', 'token', 'vs_______wvs', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'current'); + + prepare select_libraries as + select public_id::text, token_hmac, token_status::text, credential_type::text, username_attribute::text, password_attribute::text, private_key_attribute::text, private_key_passphrase_attribute::text + from credential_vault_library_issue_credentials + where public_id in ('vl______wvl2', 'vl______wvl3', 'vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7', 'vl______wvl8', 'vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12') + order by public_id; + + select results_eq( + 'select_libraries', + $$VALUES + ('vl______wvl10', 'cvs_token3'::bytea, 'current', 'ssh_private_key', 'my_username', null, null, null), + ('vl______wvl11', 'cvs_token3' , 'current', 'ssh_private_key', null, null, 'my_private_key', null), + ('vl______wvl12', 'cvs_token3' , 'current', 'ssh_private_key', 'my_username', null, 'my_private_key', 'my_passphrase'), + ('vl______wvl2', 'cvs_token3' , 'current', 'unspecified', null, null, null, null), + ('vl______wvl3', 'cvs_token3' , 'current', 'username_password', null, null, null, null), + ('vl______wvl4', 'cvs_token3' , 'current', 'username_password', null, null, null, null), + ('vl______wvl5', 'cvs_token3' , 'current', 'username_password', 'my_username', null, null, null), + ('vl______wvl6', 'cvs_token3' , 'current', 'username_password', null, 'my_password', null, null), + ('vl______wvl7', 'cvs_token3' , 'current', 'username_password', 'my_username', 'my_password', null, null), + ('vl______wvl8', 'cvs_token3' , 'current', 'ssh_private_key', null, null, null, null), + ('vl______wvl9', 'cvs_token3' , 'current', 'ssh_private_key', null, null, null, null)$$ + ); + + -- create a new current token + insert into credential_vault_token + (token_hmac, token, store_id, last_renewal_time, expiration_time, key_id, status) + values + ('cvs_token4', 'token', 'vs_______wvs', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'current'); + + select results_eq( + 'select_libraries', + $$VALUES + ('vl______wvl10', 'cvs_token4'::bytea, 'current', 'ssh_private_key', 'my_username', null, null, null), + ('vl______wvl11', 'cvs_token4' , 'current', 'ssh_private_key', null, null, 'my_private_key', null), + ('vl______wvl12', 'cvs_token4' , 'current', 'ssh_private_key', 'my_username', null, 'my_private_key', 'my_passphrase'), + ('vl______wvl2', 'cvs_token4' , 'current', 'unspecified', null, null, null, null), + ('vl______wvl3', 'cvs_token4' , 'current', 'username_password', null, null, null, null), + ('vl______wvl4', 'cvs_token4' , 'current', 'username_password', null, null, null, null), + ('vl______wvl5', 'cvs_token4' , 'current', 'username_password', 'my_username', null, null, null), + ('vl______wvl6', 'cvs_token4' , 'current', 'username_password', null, 'my_password', null, null), + ('vl______wvl7', 'cvs_token4' , 'current', 'username_password', 'my_username', 'my_password', null, null), + ('vl______wvl8', 'cvs_token4' , 'current', 'ssh_private_key', null, null, null, null), + ('vl______wvl9', 'cvs_token4' , 'current', 'ssh_private_key', null, null, null, null)$$ + ); + + -- expire token + update credential_vault_token + set status = 'expired' + where token_hmac = 'cvs_token4'; + + select results_eq( + 'select_libraries', + $$VALUES + ('vl______wvl10', null::bytea, 'expired', 'ssh_private_key', 'my_username', null, null, null), + ('vl______wvl11', null , 'expired', 'ssh_private_key', null, null, 'my_private_key', null), + ('vl______wvl12', null , 'expired', 'ssh_private_key', 'my_username', null, 'my_private_key', 'my_passphrase'), + ('vl______wvl2', null , 'expired', 'unspecified', null, null, null, null), + ('vl______wvl3', null , 'expired', 'username_password', null, null, null, null), + ('vl______wvl4', null , 'expired', 'username_password', null, null, null, null), + ('vl______wvl5', null , 'expired', 'username_password', 'my_username', null, null, null), + ('vl______wvl6', null , 'expired', 'username_password', null, 'my_password', null, null), + ('vl______wvl7', null , 'expired', 'username_password', 'my_username', 'my_password', null, null), + ('vl______wvl8', null , 'expired', 'ssh_private_key', null, null, null, null), + ('vl______wvl9', null , 'expired', 'ssh_private_key', null, null, null, null)$$ + ); + + select * from finish(); + +rollback; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql index f08dd3e273..4c2acc384d 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql @@ -2,6 +2,8 @@ -- the following triggers -- insert_credential_vault_library_mapping_override_subtype -- delete_credential_vault_library_mapping_override_subtype +-- and the following view +-- credential_vault_library_list_lookup begin; select plan(11); @@ -16,14 +18,14 @@ begin; from credential_vault_library_mapping_override where library_id in ('vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); - prepare select_private_libraries as + prepare select_libraries as select public_id::text, credential_type::text, username_attribute::text, private_key_attribute::text, private_key_passphrase_attribute::text - from credential_vault_library_private + from credential_vault_library_list_lookup where public_id in ('vl______wvl2', 'vl______wvl8', 'vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12') order by public_id; select results_eq( - 'select_private_libraries', + 'select_libraries', $$VALUES ('vl______wvl10', 'ssh_private_key', 'my_username', null, null), ('vl______wvl11', 'ssh_private_key', null, 'my_private_key', null), diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_mapping_override.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_mapping_override.sql index 5a3dced2e2..8a44cd1a3e 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_mapping_override.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_mapping_override.sql @@ -2,6 +2,8 @@ -- the following triggers -- insert_credential_vault_library_mapping_override_subtype -- delete_credential_vault_library_mapping_override_subtype +-- and the following view +-- credential_vault_library_list_lookup begin; select plan(11); @@ -16,14 +18,14 @@ begin; from credential_vault_library_mapping_override where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); - prepare select_private_libraries as + prepare select_libraries as select public_id::text, credential_type::text, username_attribute::text, password_attribute::text - from credential_vault_library_private + from credential_vault_library_list_lookup 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', + 'select_libraries', $$VALUES ('vl______wvl2', 'unspecified', null, null), ('vl______wvl3', 'username_password', null, null), diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_store_client.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_store_client.sql new file mode 100644 index 0000000000..bcf0ce09ad --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_store_client.sql @@ -0,0 +1,43 @@ +-- credential_vault_store_client tests the credential_vault_store_client view + +begin; + + select plan(5); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate the setup data + select has_view('credential_vault_store_client', 'view for client Vault stores does not exist'); + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs1'; + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs2'; + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs3'; + + -- create test vault tokens + insert into credential_vault_token + (token_hmac, token, store_id, last_renewal_time, expiration_time, key_id, status) + values + ('cvs_token2', 'token', 'vs_______cvs1', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'maintaining'), + ('cvs_token1', 'token', 'vs_______cvs1', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'current'), + ('cvs_token5', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'revoke'), + ('cvs_token4', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'maintaining'), + ('cvs_token3', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token8', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token7', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token6', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'); + + prepare select_stores as + select public_id::text, token_hmac, token_status::text + from credential_vault_store_client + where public_id in ('vs_______cvs1', 'vs_______cvs2', 'vs_______cvs3') + order by public_id, token_hmac; + + select results_eq( + 'select_stores', + $$VALUES + ('vs_______cvs1', 'cvs_token1'::bytea, 'current'), + ('vs_______cvs2', null, 'expired'), + ('vs_______cvs3', null, 'expired')$$ + ); + + select * from finish(); + +rollback; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_store_list_lookup.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_store_list_lookup.sql new file mode 100644 index 0000000000..0cdc0ce2b8 --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_store_list_lookup.sql @@ -0,0 +1,43 @@ +-- credential_vault_store_list_lookup tests the credential_vault_store_list_lookup view + +begin; + + select plan(5); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate the setup data + select has_view('credential_vault_store_list_lookup', 'view for list and lookup Vault stores does not exist'); + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs1'; + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs2'; + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs3'; + + -- create test vault tokens + insert into credential_vault_token + (token_hmac, token, store_id, last_renewal_time, expiration_time, key_id, status) + values + ('cvs_token2', 'token', 'vs_______cvs1', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'maintaining'), + ('cvs_token1', 'token', 'vs_______cvs1', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'current'), + ('cvs_token5', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'revoke'), + ('cvs_token4', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'maintaining'), + ('cvs_token3', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token8', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token7', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token6', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'); + + prepare select_stores as + select public_id::text, token_hmac, token_status::text + from credential_vault_store_list_lookup + where public_id in ('vs_______cvs1', 'vs_______cvs2', 'vs_______cvs3') + order by public_id; + + select results_eq( + 'select_stores', + $$VALUES + ('vs_______cvs1', 'cvs_token1'::bytea, 'current'), + ('vs_______cvs2', null, 'expired'), + ('vs_______cvs3', null, 'expired')$$ + ); + + select * from finish(); + +rollback; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_token_renewal_revocation.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_token_renewal_revocation.sql new file mode 100644 index 0000000000..317ddfe3bc --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_token_renewal_revocation.sql @@ -0,0 +1,44 @@ +-- credential_vault_token_renewal_revocation tests the credential_vault_token_renewal_revocation view + +begin; + + select plan(5); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate the setup data + select has_view('credential_vault_token_renewal_revocation', 'view for renewal/revocation Vault stores does not exist'); + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs1'; + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs2'; + select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______cvs3'; + + -- create test vault tokens + insert into credential_vault_token + (token_hmac, token, store_id, last_renewal_time, expiration_time, key_id, status) + values + ('cvs_token2', 'token', 'vs_______cvs1', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'maintaining'), + ('cvs_token1', 'token', 'vs_______cvs1', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'current'), + ('cvs_token5', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'revoke'), + ('cvs_token4', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'maintaining'), + ('cvs_token3', 'token', 'vs_______cvs2', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token8', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token7', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'), + ('cvs_token6', 'token', 'vs_______cvs3', now(), wt_add_seconds_to_now(1), 'kdkv___widget', 'expired'); + + prepare select_stores as + select public_id::text, token_hmac, token_status::text + from credential_vault_token_renewal_revocation + where public_id in ('vs_______cvs1', 'vs_______cvs2', 'vs_______cvs3') + order by public_id, token_hmac; + + select results_eq( + 'select_stores', + $$VALUES + ('vs_______cvs1', 'cvs_token1'::bytea, 'current'), + ('vs_______cvs1', 'cvs_token2'::bytea, 'maintaining'), + ('vs_______cvs2', 'cvs_token4'::bytea, 'maintaining'), + ('vs_______cvs2', 'cvs_token5'::bytea, 'revoke')$$ + ); + + select * from finish(); + +rollback; diff --git a/internal/proto/controller/api/resources/credentialstores/v1/credential_store.proto b/internal/proto/controller/api/resources/credentialstores/v1/credential_store.proto index 524838e633..2b16dbc05b 100644 --- a/internal/proto/controller/api/resources/credentialstores/v1/credential_store.proto +++ b/internal/proto/controller/api/resources/credentialstores/v1/credential_store.proto @@ -173,4 +173,7 @@ message VaultCredentialStoreAttributes { that: "WorkerFilter" } ]; // @gotags: `class:"public"` + + // Output only. The status of the vault token used by this credential store (current or expired). + string token_status = 120 [json_name = "token_status"]; // @gotags: `class:"public"` } diff --git a/internal/tests/api/credentialstores/credentialstore_test.go b/internal/tests/api/credentialstores/credentialstore_test.go index dc5e077bfe..05500d61e7 100644 --- a/internal/tests/api/credentialstores/credentialstore_test.go +++ b/internal/tests/api/credentialstores/credentialstore_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/api/credentialstores" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/daemon/controller" + "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/iam" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -100,7 +101,7 @@ func TestCrud(t *testing.T) { client.SetToken(token.Token) _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId)) - checkResource := func(step string, cs *credentialstores.CredentialStore, err error, wantedName string, wantVersion uint32) { + checkResource := func(step string, cs *credentialstores.CredentialStore, err error, wantedName string, wantedTokenStatus vault.TokenStatus, wantVersion uint32) { assert.NotNil(cs, "returned no resource", step) gotName := "" if cs.Name != "" { @@ -108,6 +109,9 @@ func TestCrud(t *testing.T) { } assert.Equal(wantedName, gotName, step) assert.Equal(wantVersion, cs.Version) + status, ok := cs.Attributes["token_status"].(string) + require.True(ok) + assert.Equal(string(wantedTokenStatus), status) } csClient := credentialstores.NewClient(client) @@ -116,16 +120,34 @@ func TestCrud(t *testing.T) { credentialstores.WithVaultCredentialStoreAddress(vaultServ.Addr), credentialstores.WithVaultCredentialStoreToken(vaultTok)) require.NoError(err) require.NotNil(cs) - checkResource("create", cs.Item, err, "foo", 1) + checkResource("create", cs.Item, err, "foo", vault.CurrentToken, 1) cs, err = csClient.Read(tc.Context(), cs.Item.Id) - checkResource("read", cs.Item, err, "foo", 1) + checkResource("read", cs.Item, err, "foo", vault.CurrentToken, 1) cs, err = csClient.Update(tc.Context(), cs.Item.Id, cs.Item.Version, credentialstores.WithName("bar")) - checkResource("update", cs.Item, err, "bar", 2) + checkResource("update", cs.Item, err, "bar", vault.CurrentToken, 2) cs, err = csClient.Update(tc.Context(), cs.Item.Id, cs.Item.Version, credentialstores.DefaultName()) - checkResource("update", cs.Item, err, "", 3) + checkResource("update", cs.Item, err, "", vault.CurrentToken, 3) + + // Set previous token to expired in the database and revoke in Vault to validate a + // credential store with an expired token is correctly returned over the API + rw := db.New(tc.DbConn()) + num, err := rw.Exec(tc.Context(), "update credential_vault_token set status = ? where store_id = ?", + []interface{}{vault.ExpiredToken, cs.GetItem().Id}) + require.NoError(err) + assert.Equal(1, num) + vaultServ.RevokeToken(t, vaultTok) + + cs, err = csClient.Read(tc.Context(), cs.Item.Id) + checkResource("read", cs.Item, err, "", vault.ExpiredToken, 3) + + // Create a new token and update cred store + _, newVaultTok := vaultServ.CreateToken(t) + cs, err = csClient.Update(tc.Context(), cs.Item.Id, cs.Item.Version, credentialstores.WithName("bar"), + credentialstores.WithVaultCredentialStoreToken(newVaultTok)) + checkResource("update", cs.Item, err, "bar", vault.CurrentToken, 4) _, err = csClient.Delete(tc.Context(), cs.Item.Id) assert.NoError(err) diff --git a/sdk/pbs/controller/api/resources/credentialstores/credential_store.pb.go b/sdk/pbs/controller/api/resources/credentialstores/credential_store.pb.go index 45da3563a3..62e649c92e 100644 --- a/sdk/pbs/controller/api/resources/credentialstores/credential_store.pb.go +++ b/sdk/pbs/controller/api/resources/credentialstores/credential_store.pb.go @@ -240,6 +240,8 @@ type VaultCredentialStoreAttributes struct { ClientCertificateKeyHmac string `protobuf:"bytes,100,opt,name=client_certificate_key_hmac,proto3" json:"client_certificate_key_hmac,omitempty" class:"public"` // @gotags: `class:"public"` // worker_filter is optional. Filters to the worker(s) who can handle Vault requests for this cred store WorkerFilter *wrapperspb.StringValue `protobuf:"bytes,110,opt,name=worker_filter,proto3" json:"worker_filter,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. The status of the vault token used by this credential store (current or expired). + TokenStatus string `protobuf:"bytes,120,opt,name=token_status,proto3" json:"token_status,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *VaultCredentialStoreAttributes) Reset() { @@ -351,6 +353,13 @@ func (x *VaultCredentialStoreAttributes) GetWorkerFilter() *wrapperspb.StringVal return nil } +func (x *VaultCredentialStoreAttributes) GetTokenStatus() string { + if x != nil { + return x.TokenStatus + } + return "" +} + var File_controller_api_resources_credentialstores_v1_credential_store_proto protoreflect.FileDescriptor var file_controller_api_resources_credentialstores_v1_credential_store_proto_rawDesc = []byte{ @@ -442,7 +451,7 @@ var file_controller_api_resources_credentialstores_v1_credential_store_proto_raw 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, - 0x89, 0x09, 0x0a, 0x1e, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0xad, 0x09, 0x0a, 0x1e, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x62, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, @@ -514,14 +523,16 @@ var file_controller_api_resources_credentialstores_v1_credential_store_proto_raw 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x0c, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x0d, 0x77, 0x6f, - 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x62, 0x5a, 0x60, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, - 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x73, 0x3b, 0x63, - 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x73, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x22, 0x0a, 0x0c, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x78, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, + 0x62, 0x5a, 0x60, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, + 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var (