From 7833dd73c07144ee93e42ac958ae7363b7c82450 Mon Sep 17 00:00:00 2001 From: Danielle <29378233+DanielleMiu@users.noreply.github.com> Date: Mon, 9 Jan 2023 12:06:43 -0500 Subject: [PATCH 01/11] refact(vault): Create issuingCredentialLibrary Interface Allows for new issuing credential library implementations in the future. Moved db and decrypt logic to privateCredentialLibraryAllTypes from issueCredentialLibrary. This also removes sessionId from retrieveCredential, it was not needed and removing allows retrieveCredential to be reused for credentials issued outside current workflow. --- internal/credential/vault/credential.go | 40 +-- internal/credential/vault/credential_test.go | 16 +- internal/credential/vault/private_library.go | 241 +++++++++++++----- .../credential/vault/private_library_test.go | 93 +++---- .../vault/repository_credentials.go | 49 +++- .../vault/repository_credentials_test.go | 58 +++++ internal/credential/vault/testing.go | 4 +- 7 files changed, 338 insertions(+), 163 deletions(-) diff --git a/internal/credential/vault/credential.go b/internal/credential/vault/credential.go index 10f390f8e..5ca58ece9 100644 --- a/internal/credential/vault/credential.go +++ b/internal/credential/vault/credential.go @@ -1,10 +1,8 @@ package vault import ( - "database/sql" "time" - "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/store" "github.com/hashicorp/boundary/internal/db/sanitize" "github.com/hashicorp/boundary/internal/db/sentinel" @@ -46,14 +44,11 @@ type Credential struct { expiration time.Duration `gorm:"-"` } -func newCredential(libraryId, sessionId, externalId string, tokenHmac []byte, expiration time.Duration) (*Credential, error) { +func newCredential(libraryId, externalId string, tokenHmac []byte, expiration time.Duration) (*Credential, error) { const op = "vault.newCredential" if libraryId == "" { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no library id") } - if sessionId == "" { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no session id") - } if len(tokenHmac) == 0 { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no tokenHmac") } @@ -69,7 +64,6 @@ func newCredential(libraryId, sessionId, externalId string, tokenHmac []byte, ex expiration: expiration.Round(time.Second), Credential: &store.Credential{ LibraryId: libraryId, - SessionId: sessionId, ExternalId: externalId, TokenHmac: tokenHmac, Status: status, @@ -117,38 +111,6 @@ func (c *Credential) oplog(op oplog.OpType) oplog.Metadata { return metadata } -func (c *Credential) insertQuery() (query string, queryValues []any) { - queryValues = []any{ - sql.Named("public_id", c.PublicId), - sql.Named("library_id", c.LibraryId), - sql.Named("session_id", c.SessionId), - sql.Named("token_hmac", c.TokenHmac), - sql.Named("external_id", c.ExternalId), - sql.Named("is_renewable", c.IsRenewable), - sql.Named("status", c.Status), - sql.Named("last_renewal_time", "now()"), - } - switch { - case c.expiration == 0: - query = insertCredentialWithInfiniteExpirationQuery - default: - query = insertCredentialWithExpirationQuery - queryValues = append(queryValues, sql.Named("expiration_time", int(c.expiration.Round(time.Second).Seconds()))) - } - return -} - -func (c *Credential) updateSessionQuery(purpose credential.Purpose) (query string, queryValues []any) { - queryValues = []any{ - sql.Named("public_id", c.PublicId), - sql.Named("library_id", c.LibraryId), - sql.Named("session_id", c.SessionId), - sql.Named("purpose", string(purpose)), - } - query = updateSessionCredentialQuery - return -} - func (c *Credential) updateExpirationQuery() (query string, queryValues []any) { queryValues = []any{ int(c.expiration.Round(time.Second).Seconds()), diff --git a/internal/credential/vault/credential_test.go b/internal/credential/vault/credential_test.go index 07b6e795b..9666d9f4e 100644 --- a/internal/credential/vault/credential_test.go +++ b/internal/credential/vault/credential_test.go @@ -56,17 +56,6 @@ func TestCredential_New(t *testing.T) { want: nil, wantErr: true, }, - { - name: "missing-session-id", - args: args{ - libraryId: lib.GetPublicId(), - externalId: "some/vault/credential", - tokenHmac: token.GetTokenHmac(), - expiration: 5 * time.Minute, - }, - want: nil, - wantErr: true, - }, { name: "missing-tokenHmac", args: args{ @@ -144,7 +133,7 @@ func TestCredential_New(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) ctx := context.Background() - got, err := newCredential(tt.args.libraryId, tt.args.sessionId, + got, err := newCredential(tt.args.libraryId, tt.args.externalId, tt.args.tokenHmac, tt.args.expiration) if tt.wantErr { assert.Error(err) @@ -162,7 +151,8 @@ func TestCredential_New(t *testing.T) { tt.want.PublicId = id got.PublicId = id - query, queryValues := got.insertQuery() + query, queryValues := insertQuery(got, tt.args.sessionId) + require.NoError(err) rows, err2 := rw.Exec(ctx, query, queryValues) assert.Equal(1, rows) diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 274194126..712b84765 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -25,7 +25,7 @@ var _ credential.Dynamic = (*baseCred)(nil) type baseCred struct { *Credential - lib *issueCredentialLibrary + lib *genericIssuingCredentialLibrary secretData map[string]any } @@ -33,6 +33,7 @@ func (bc *baseCred) Secret() credential.SecretData { return bc.secretData } func (bc *baseCred) Library() credential.Library { return bc.lib } func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.Purpose } func (bc *baseCred) getExpiration() time.Duration { return bc.expiration } +func (bc *baseCred) getCredential() *Credential { return bc.Credential } // convert converts bc to a specific credential type if bc is not // UnspecifiedType. @@ -132,12 +133,17 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err }, nil } -var _ credential.Library = (*issueCredentialLibrary)(nil) +var _ credential.Library = (*genericIssuingCredentialLibrary)(nil) -// A issueCredentialLibrary contains all the values needed to connect to Vault and +type issuingCredentialLibrary interface { + GetPublicId() string + retrieveCredential(context.Context, errors.Op, ...credential.Option) (dynamicCred, error) +} + +// A genericIssuingCredentialLibrary contains all the values needed to connect to Vault and // retrieve credentials. -type issueCredentialLibrary struct { - PublicId string `gorm:"primary_key"` +type genericIssuingCredentialLibrary struct { + PublicId string StoreId string Name string Description string @@ -147,7 +153,7 @@ type issueCredentialLibrary struct { VaultPath string HttpMethod string HttpRequestBody []byte - CredType string `gorm:"column:credential_type"` + CredType string ProjectId string VaultAddress string Namespace string @@ -167,13 +173,13 @@ type issueCredentialLibrary struct { PasswordAttribute string PrivateKeyAttribute string PrivateKeyPassphraseAttribute string - Purpose credential.Purpose `gorm:"-"` + Purpose credential.Purpose } -func (pl *issueCredentialLibrary) clone() *issueCredentialLibrary { +func (pl *genericIssuingCredentialLibrary) clone() *genericIssuingCredentialLibrary { // The 'append(a[:0:0], a...)' comes from // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F - return &issueCredentialLibrary{ + return &genericIssuingCredentialLibrary{ PublicId: pl.PublicId, StoreId: pl.StoreId, CredType: pl.CredType, @@ -208,15 +214,15 @@ func (pl *issueCredentialLibrary) clone() *issueCredentialLibrary { } } -func (pl *issueCredentialLibrary) GetPublicId() string { return pl.PublicId } -func (pl *issueCredentialLibrary) GetStoreId() string { return pl.StoreId } -func (pl *issueCredentialLibrary) GetName() string { return pl.Name } -func (pl *issueCredentialLibrary) GetDescription() string { return pl.Description } -func (pl *issueCredentialLibrary) GetVersion() uint32 { return pl.Version } -func (pl *issueCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } -func (pl *issueCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } +func (pl *genericIssuingCredentialLibrary) GetPublicId() string { return pl.PublicId } +func (pl *genericIssuingCredentialLibrary) GetStoreId() string { return pl.StoreId } +func (pl *genericIssuingCredentialLibrary) GetName() string { return pl.Name } +func (pl *genericIssuingCredentialLibrary) GetDescription() string { return pl.Description } +func (pl *genericIssuingCredentialLibrary) GetVersion() uint32 { return pl.Version } +func (pl *genericIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } +func (pl *genericIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } -func (pl *issueCredentialLibrary) CredentialType() credential.Type { +func (pl *genericIssuingCredentialLibrary) CredentialType() credential.Type { switch ct := pl.CredType; ct { case "": return credential.UnspecifiedType @@ -225,41 +231,8 @@ func (pl *issueCredentialLibrary) CredentialType() credential.Type { } } -func (pl *issueCredentialLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { - const op = "vault.(issueCredentialLibrary).decrypt" - - if pl.CtToken != nil { - type ptk struct { - Token []byte `wrapping:"pt,token_data"` - CtToken []byte `wrapping:"ct,token_data"` - } - ptkv := &ptk{ - CtToken: pl.CtToken, - } - if err := structwrapping.UnwrapStruct(ctx, cipher, ptkv, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("token")) - } - pl.Token = ptkv.Token - } - - if pl.CtClientKey != nil && pl.ClientCert != nil { - type pck struct { - Key []byte `wrapping:"pt,key_data"` - CtKey []byte `wrapping:"ct,key_data"` - } - pckv := &pck{ - CtKey: pl.CtClientKey, - } - if err := structwrapping.UnwrapStruct(ctx, cipher, pckv, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("client certificate")) - } - pl.ClientKey = pckv.Key - } - return nil -} - -func (pl *issueCredentialLibrary) client(ctx context.Context) (vaultClient, error) { - const op = "vault.(issueCredentialLibrary).client" +func (pl *genericIssuingCredentialLibrary) client(ctx context.Context) (vaultClient, error) { + const op = "vault.(genericIssuingCredentialLibrary).client" clientConfig := &clientConfig{ Addr: pl.VaultAddress, Token: pl.Token, @@ -284,15 +257,14 @@ func (pl *issueCredentialLibrary) client(ctx context.Context) (vaultClient, erro type dynamicCred interface { credential.Dynamic getExpiration() time.Duration - insertQuery() (query string, queryValues []any) - updateSessionQuery(purpose credential.Purpose) (query string, queryValues []any) + getCredential() *Credential } // retrieveCredential retrieves a dynamic credential from Vault for the // given sessionId. // // Supported options: credential.WithTemplateData -func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, sessionId string, opt ...credential.Option) (dynamicCred, error) { +func (pl *genericIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, opt ...credential.Option) (dynamicCred, error) { opts, err := credential.GetOpts(opt...) if err != nil { return nil, errors.Wrap(ctx, err, op) @@ -358,7 +330,7 @@ func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op err } leaseDuration := time.Duration(secret.LeaseDuration) * time.Second - cred, err := newCredential(pl.GetPublicId(), sessionId, secret.LeaseID, pl.TokenHmac, leaseDuration) + cred, err := newCredential(pl.GetPublicId(), secret.LeaseID, pl.TokenHmac, leaseDuration) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -374,11 +346,11 @@ func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op err } // TableName returns the table name for gorm. -func (pl *issueCredentialLibrary) TableName() string { +func (pl *genericIssuingCredentialLibrary) TableName() string { return "credential_vault_library_issue_credentials" } -func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]*issueCredentialLibrary, error) { +func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]issuingCredentialLibrary, error) { const op = "vault.(Repository).getIssueCredLibraries" mapper, err := newMapper(requests) @@ -405,9 +377,9 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede } defer rows.Close() - var libs []*issueCredentialLibrary + var libs []*privateCredentialLibraryAllTypes for rows.Next() { - var lib issueCredentialLibrary + var lib privateCredentialLibraryAllTypes if err := r.reader.ScanRows(ctx, rows, &lib); err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("scan row failed")) } @@ -422,6 +394,7 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede } } + var decryptedLibs []issuingCredentialLibrary for _, pl := range libs { databaseWrapper, err := r.kms.GetWrapper(ctx, pl.ProjectId, kms.KeyPurposeDatabase) if err != nil { @@ -431,9 +404,155 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede if err := pl.decrypt(ctx, databaseWrapper); err != nil { return nil, errors.Wrap(ctx, err, op) } + decryptedLibs = append(decryptedLibs, pl.toTypedIssuingCredentialLibrary()) } - return libs, nil + return decryptedLibs, nil +} + +// privateCredentialLibraryAllTypes is a clone of genericIssuingCredentialLibrary. Contains +// all the values needed to connect to Vault and retrieve credentials. +type privateCredentialLibraryAllTypes struct { + PublicId string `gorm:"primary_key"` + StoreId string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + VaultPath string + HttpMethod string + HttpRequestBody []byte + CredType string `gorm:"column:credential_type"` + ProjectId string + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + WorkerFilter string + Token TokenSecret + CtToken []byte + TokenHmac []byte + TokenKeyId string + ClientCert []byte + ClientKey KeySecret + CtClientKey []byte + ClientKeyId string + UsernameAttribute string + PasswordAttribute string + PrivateKeyAttribute string + PrivateKeyPassphraseAttribute string + Purpose credential.Purpose `gorm:"-"` +} + +func (pl *privateCredentialLibraryAllTypes) GetPublicId() string { return pl.PublicId } + +func (pl *privateCredentialLibraryAllTypes) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "vault.(privateCredentialLibraryAllTypes).decrypt" + + if pl.CtToken != nil { + type ptk struct { + Token []byte `wrapping:"pt,token_data"` + CtToken []byte `wrapping:"ct,token_data"` + } + ptkv := &ptk{ + CtToken: pl.CtToken, + } + if err := structwrapping.UnwrapStruct(ctx, cipher, ptkv, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("token")) + } + pl.Token = ptkv.Token + } + + if pl.CtClientKey != nil && pl.ClientCert != nil { + type pck struct { + Key []byte `wrapping:"pt,key_data"` + CtKey []byte `wrapping:"ct,key_data"` + } + pckv := &pck{ + CtKey: pl.CtClientKey, + } + if err := structwrapping.UnwrapStruct(ctx, cipher, pckv, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("client certificate")) + } + pl.ClientKey = pckv.Key + } + return nil +} + +func (pl *privateCredentialLibraryAllTypes) clone() *privateCredentialLibraryAllTypes { + // The 'append(a[:0:0], a...)' comes from + // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F + return &privateCredentialLibraryAllTypes{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + PrivateKeyAttribute: pl.PrivateKeyAttribute, + PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), + UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), + Version: pl.Version, + ProjectId: pl.ProjectId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...), + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: append(pl.CaCert[:0:0], pl.CaCert...), + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + WorkerFilter: pl.WorkerFilter, + TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...), + Token: append(pl.Token[:0:0], pl.Token...), + CtToken: append(pl.CtToken[:0:0], pl.CtToken...), + TokenKeyId: pl.TokenKeyId, + ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...), + ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...), + CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, + } +} + +func (pl *privateCredentialLibraryAllTypes) toTypedIssuingCredentialLibrary() issuingCredentialLibrary { + return &genericIssuingCredentialLibrary{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + PrivateKeyAttribute: pl.PrivateKeyAttribute, + PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: pl.CreateTime, + UpdateTime: pl.UpdateTime, + Version: pl.Version, + ProjectId: pl.ProjectId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: pl.HttpRequestBody, + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: pl.CaCert, + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + WorkerFilter: pl.WorkerFilter, + TokenHmac: pl.TokenHmac, + Token: pl.Token, + CtToken: pl.CtToken, + TokenKeyId: pl.TokenKeyId, + ClientCert: pl.ClientCert, + ClientKey: pl.ClientKey, + CtClientKey: pl.CtClientKey, + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, + } } // requestMap takes a slice of credential requests and provides of list of diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index fc156232b..dca8b671c 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -269,7 +269,10 @@ func TestRepository_getPrivateLibraries(t *testing.T) { require.NotNil(gotLibs) assert.Len(gotLibs, len(libs)) - for _, got := range gotLibs { + for _, gotLib := range gotLibs { + got, ok := gotLib.(*genericIssuingCredentialLibrary) + require.True(ok) + assert.Equal(TokenSecret(token), got.Token) if tt.tls == TestClientTLS { require.NotNil(got.ClientKey) @@ -414,7 +417,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "library-not-username-password-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UnspecifiedType), }, }, @@ -423,7 +426,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-no-username-default-password-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -435,7 +438,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-no-password-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -447,7 +450,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -463,7 +466,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", PasswordAttribute: "test-password", @@ -483,7 +486,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-default-username-override-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), PasswordAttribute: "test-password", }, @@ -502,7 +505,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-override-username-default-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", }, @@ -521,7 +524,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-username-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "missing-username", }, @@ -537,7 +540,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-password-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "missing-password", }, @@ -553,7 +556,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-metadata-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -568,7 +571,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-data-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -580,7 +583,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-username-default-password-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -595,7 +598,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-passsword-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -610,7 +613,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -626,7 +629,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -639,7 +642,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-additional-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -656,7 +659,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -675,7 +678,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", PasswordAttribute: "test-password", @@ -698,7 +701,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-default-username-override-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), PasswordAttribute: "test-password", }, @@ -720,7 +723,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-override-username-default-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", }, @@ -779,7 +782,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "library-not-ssh-private-key-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UnspecifiedType), }, }, @@ -788,7 +791,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-no-username-default-pk-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -800,7 +803,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-no-pk-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -812,7 +815,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -828,7 +831,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -846,7 +849,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -866,7 +869,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -890,7 +893,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-username-override-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), PrivateKeyAttribute: "test-pk", }, @@ -909,7 +912,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-username-default-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", }, @@ -928,7 +931,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-username-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "missing-username", }, @@ -944,7 +947,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-pk-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "missing-pk", }, @@ -960,7 +963,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-metadata-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -975,7 +978,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-data-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -987,7 +990,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-username-default-pk-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1002,7 +1005,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-pk-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1017,7 +1020,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1033,7 +1036,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-invalid-data-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1046,7 +1049,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-additional-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1063,7 +1066,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1082,7 +1085,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1103,7 +1106,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -1126,7 +1129,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -1153,7 +1156,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-username-override-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), PrivateKeyAttribute: "test-pk", }, @@ -1175,7 +1178,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-username-default-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", }, diff --git a/internal/credential/vault/repository_credentials.go b/internal/credential/vault/repository_credentials.go index 8d72895ac..ab715847e 100644 --- a/internal/credential/vault/repository_credentials.go +++ b/internal/credential/vault/repository_credentials.go @@ -2,6 +2,7 @@ package vault import ( "context" + "database/sql" "fmt" "time" @@ -13,6 +14,38 @@ import ( var _ credential.Issuer = (*Repository)(nil) +func insertQuery(c *Credential, sessionId string) (query string, queryValues []any) { + queryValues = []any{ + sql.Named("public_id", c.PublicId), + sql.Named("library_id", c.LibraryId), + sql.Named("session_id", sessionId), + sql.Named("token_hmac", c.TokenHmac), + sql.Named("external_id", c.ExternalId), + sql.Named("is_renewable", c.IsRenewable), + sql.Named("status", c.Status), + sql.Named("last_renewal_time", "now()"), + } + switch { + case c.expiration == 0: + query = insertCredentialWithInfiniteExpirationQuery + default: + query = insertCredentialWithExpirationQuery + queryValues = append(queryValues, sql.Named("expiration_time", int(c.expiration.Round(time.Second).Seconds()))) + } + return +} + +func updateSessionQuery(c *Credential, sessionId string, purpose credential.Purpose) (query string, queryValues []any) { + queryValues = []any{ + sql.Named("public_id", c.PublicId), + sql.Named("library_id", c.LibraryId), + sql.Named("session_id", sessionId), + sql.Named("purpose", string(purpose)), + } + query = updateSessionCredentialQuery + return +} + // Issue issues and returns dynamic credentials from Vault for all of the // requests and assigns them to sessionId. func (r *Repository) Issue(ctx context.Context, sessionId string, requests []credential.Request, opt ...credential.Option) ([]credential.Dynamic, error) { @@ -37,7 +70,7 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre var minLease time.Duration runJobsInterval := r.scheduler.GetRunJobsInterval() for _, lib := range libs { - cred, err := lib.retrieveCredential(ctx, op, sessionId, opt...) + cred, err := lib.retrieveCredential(ctx, op, opt...) if err != nil { return nil, err } @@ -54,8 +87,18 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre if minLease > cred.getExpiration() { minLease = cred.getExpiration() } - insertQuery, insertQueryValues := cred.insertQuery() - updateQuery, updateQueryValues := cred.updateSessionQuery(lib.Purpose) + + underlyingCred := cred.getCredential() + + insertQuery, insertQueryValues := insertQuery(underlyingCred, sessionId) + if err != nil { + return nil, err + } + updateQuery, updateQueryValues := updateSessionQuery(underlyingCred, sessionId, cred.Purpose()) + if err != nil { + return nil, err + } + if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error { rowsInserted, err := w.Exec(ctx, insertQuery, insertQueryValues) diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 998ec15ee..0d66bf1b9 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" @@ -21,6 +22,58 @@ import ( "github.com/stretchr/testify/require" ) +// since we're not using gorm tags to retrieve this, it's faster and easier to just make a new struct than import usrPassCred from the vault package +type userPassCred struct { + PublicId string + LibraryId string + SessionId string + TokenHmac []byte + ExternalId string + IsRenewable bool + Status string + LastRenewalTime *timestamp.Timestamp + ExpirationTime *timestamp.Timestamp +} + +func lookupDbCred(t *testing.T, ctx context.Context, rw *db.Db, upc credential.UsernamePassword) userPassCred { + rows, err := rw.Query(ctx, ` + select + public_id, + library_id, + session_id, + token_hmac, + external_id, + is_renewable, + status, + last_renewal_time, + expiration_time + from credential_vault_credential + where public_id = ?; + `, []any{upc.GetPublicId()}) + require.NoError(t, err) + rowCount := 0 + + got := userPassCred{} + + for rows.Next() { + rowCount++ + require.NoError(t, rows.Scan( + &got.PublicId, + &got.LibraryId, + &got.SessionId, + &got.TokenHmac, + &got.ExternalId, + &got.IsRenewable, + &got.Status, + &got.LastRenewalTime, + &got.ExpirationTime, + )) + } + assert.Equal(t, 1, rowCount) + + return got +} + func TestRepository_IssueCredentials(t *testing.T) { t.Parallel() ctx := context.Background() @@ -398,6 +451,11 @@ func TestRepository_IssueCredentials(t *testing.T) { if upc, ok := dc.(credential.UsernamePassword); ok { assert.NotEmpty(upc.Username()) assert.NotEmpty(upc.Password()) + // we also want to retrieve the cred from the db and make sure it's the same as the one returned by Issue + retrieved := lookupDbCred(t, ctx, rw, upc) + require.Equal(retrieved.SessionId, sess.GetPublicId()) + require.Equal(retrieved.PublicId, upc.GetPublicId()) + // these asserts are super basic because the main check we want is that lookupDbCred succeeded break } assert.Fail("want UsernamePassword credential from library with credential type UsernamePassword") diff --git a/internal/credential/vault/testing.go b/internal/credential/vault/testing.go index bec523201..817492655 100644 --- a/internal/credential/vault/testing.go +++ b/internal/credential/vault/testing.go @@ -168,7 +168,7 @@ func TestCredentials(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, librar var credentials []*Credential for i := 0; i < count; i++ { - credential, err := newCredential(lib.GetPublicId(), sessionId, fmt.Sprintf("vault/credential/%d", i), token.GetTokenHmac(), 5*time.Minute) + credential, err := newCredential(lib.GetPublicId(), fmt.Sprintf("vault/credential/%d", i), token.GetTokenHmac(), 5*time.Minute) assert.NoError(err) require.NotNil(credential) @@ -177,7 +177,7 @@ func TestCredentials(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, librar require.NotNil(id) credential.PublicId = id - query, queryValues := credential.insertQuery() + query, queryValues := insertQuery(credential, sessionId) rows, err2 := rw.Exec(ctx, query, queryValues) assert.Equal(1, rows) assert.NoError(err2) From 823b8cdfc68d389199b3d2d6d9b6c0e0411cf824 Mon Sep 17 00:00:00 2001 From: Haotian Date: Wed, 11 Jan 2023 09:08:22 -0800 Subject: [PATCH 02/11] feat(vault): Add CRUDL methods for ssh certificate cred library (#2765) This defines a new credential library type for ssh certificates. It adds new repository methods to support CRUDL of the new ssh certificate credential library. Co-authored-by: Timothy Messier --- internal/credential/credential.go | 1 + internal/credential/vault/fields.go | 12 + internal/credential/vault/options.go | 52 + internal/credential/vault/public_ids.go | 27 +- ...tory_ssh_certificate_credential_library.go | 363 +++ ...ssh_certificate_credential_library_test.go | 1948 +++++++++++++++++ .../credential/vault/setup_manual_test.go | 2 +- .../ssh_certificate_credential_library.go | 120 + internal/credential/vault/store/vault.pb.go | 482 +++- internal/credential/vault/testing.go | 32 + .../39/01_static_ssh_private_key_creds.up.sql | 1 + ...1_credential_vault_ssh_cert_library.up.sql | 170 ++ .../credential_vault_ssh_cert_library.sql | 148 ++ .../credential/vault/store/v1/vault.proto | 101 + 14 files changed, 3351 insertions(+), 108 deletions(-) create mode 100644 internal/credential/vault/repository_ssh_certificate_credential_library.go create mode 100644 internal/credential/vault/repository_ssh_certificate_credential_library_test.go create mode 100644 internal/credential/vault/ssh_certificate_credential_library.go create mode 100644 internal/db/schema/migrations/oss/postgres/63/01_credential_vault_ssh_cert_library.up.sql create mode 100644 internal/db/sqltest/tests/credential/vault/credential_vault_ssh_cert_library.sql diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 0c8726eef..d09e4cc60 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -30,6 +30,7 @@ const ( UnspecifiedType Type = "unspecified" UsernamePasswordType Type = "username_password" SshPrivateKeyType Type = "ssh_private_key" + SshCertificateType Type = "ssh_certificate" JsonType Type = "json" ) diff --git a/internal/credential/vault/fields.go b/internal/credential/vault/fields.go index a3fab7300..f4e9ec076 100644 --- a/internal/credential/vault/fields.go +++ b/internal/credential/vault/fields.go @@ -9,6 +9,18 @@ const ( httpMethodField = "HttpMethod" httpRequestBodyField = "HttpRequestBody" + usernameField = "Username" + keyTypeField = "KeyType" + keyBitsField = "KeyBits" + ttlField = "Ttl" + keyIdField = "KeyId" + // CriticalOptionsField represents the field mask indicating a critical option + // update has been requested. + CriticalOptionsField = "CriticalOptions" + // ExtensionsField represents the field mask indicating an extension + // update has been requested. + ExtensionsField = "Extensions" + certificateField = "Certificate" certificateKeyField = "CertificateKey" vaultAddressField = "VaultAddress" diff --git a/internal/credential/vault/options.go b/internal/credential/vault/options.go index cc21022fd..c97a46b78 100644 --- a/internal/credential/vault/options.go +++ b/internal/credential/vault/options.go @@ -34,6 +34,13 @@ type options struct { withOverridePrivateKeyAttribute string withOverridePrivateKeyPassphraseAttribute string withMappingOverride MappingOverride + + withKeyType string + withKeyBits uint32 + withTtl string + withKeyId string + withCriticalOptions string + withExtensions string } func getDefaultOptions() options { @@ -174,3 +181,48 @@ func WithMappingOverride(m MappingOverride) Option { o.withMappingOverride = m } } + +// WithKeyType provides an optional ssh private key type to use +// with a ssh certificate credential library. Must be rsa, ed25519, or ecdsa. +func WithKeyType(t string) Option { + return func(o *options) { + o.withKeyType = t + } +} + +// WithKeyBits provides an optional number of bits used to generate an ssh private key. +func WithKeyBits(b uint32) Option { + return func(o *options) { + o.withKeyBits = b + } +} + +// WithTtl provides an optional requested time to live for a generated ssh certificate. +func WithTtl(t string) Option { + return func(o *options) { + o.withTtl = t + } +} + +// WithKeyId provides an optional key id for a created certificate. +func WithKeyId(i string) Option { + return func(o *options) { + o.withKeyId = i + } +} + +// WithCriticalOptions provides an optional map of the critical options +// that the certificate should be signed for. +func WithCriticalOptions(s string) Option { + return func(o *options) { + o.withCriticalOptions = s + } +} + +// WithExtensions provides a optional map of the extensions +// that the certificate should be signed for. +func WithExtensions(s string) Option { + return func(o *options) { + o.withExtensions = s + } +} diff --git a/internal/credential/vault/public_ids.go b/internal/credential/vault/public_ids.go index 4cbec8374..0587c8534 100644 --- a/internal/credential/vault/public_ids.go +++ b/internal/credential/vault/public_ids.go @@ -8,18 +8,27 @@ import ( ) func init() { - if err := subtypes.Register(credential.Domain, Subtype, CredentialStorePrefix, CredentialLibraryPrefix, DynamicCredentialPrefix); err != nil { + if err := subtypes.Register(credential.Domain, Subtype, CredentialStorePrefix, DynamicCredentialPrefix); err != nil { + panic(err) + } + if err := subtypes.Register(credential.Domain, GenericLibrarySubtype, CredentialLibraryPrefix); err != nil { + panic(err) + } + if err := subtypes.Register(credential.Domain, SSHCertificateLibrarySubtype, SSHCertificateCredentialLibraryPrefix); err != nil { panic(err) } } // PublicId prefixes for the resources in the vault package. const ( - CredentialStorePrefix = "csvlt" - CredentialLibraryPrefix = "clvlt" - DynamicCredentialPrefix = "cdvlt" + CredentialStorePrefix = "csvlt" + CredentialLibraryPrefix = "clvlt" + DynamicCredentialPrefix = "cdvlt" + SSHCertificateCredentialLibraryPrefix = "clvsclt" - Subtype = subtypes.Subtype("vault") + Subtype = subtypes.Subtype("vault") + GenericLibrarySubtype = subtypes.Subtype("vault-generic") + SSHCertificateLibrarySubtype = subtypes.Subtype("vault-ssh-certificate") ) func newCredentialStoreId() (string, error) { @@ -45,3 +54,11 @@ func newCredentialLibraryId() (string, error) { } return id, nil } + +func newSSHCertificateCredentialLibraryId() (string, error) { + id, err := db.NewPublicId(SSHCertificateCredentialLibraryPrefix) + if err != nil { + return "", errors.WrapDeprecated(err, "vault.newSSHCertificateCredentialLibraryPrefix") + } + return id, nil +} diff --git a/internal/credential/vault/repository_ssh_certificate_credential_library.go b/internal/credential/vault/repository_ssh_certificate_credential_library.go new file mode 100644 index 000000000..f26b2ef60 --- /dev/null +++ b/internal/credential/vault/repository_ssh_certificate_credential_library.go @@ -0,0 +1,363 @@ +package vault + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/go-dbw" +) + +// CreateSSHCertificateCredentialLibrary inserts l into the repository and returns a new +// SSHCertificateCredentialLibrary containing the credential library's PublicId. l is not +// changed. l must contain a valid StoreId. l must not contain a PublicId. +// The PublicId is generated and assigned by this method. +// +// Both l.Name and l.Description are optional. If l.Name is set, it must be +// unique within l.StoreId. +// +// Both l.CreateTime and l.UpdateTime are ignored. +func (r *Repository) CreateSSHCertificateCredentialLibrary(ctx context.Context, projectId string, l *SSHCertificateCredentialLibrary, _ ...Option) (*SSHCertificateCredentialLibrary, error) { + const op = "vault.(Repository).CreateSSHCertificateCredentialLibrary" + if l == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil SSHCertificateCredentialLibrary") + } + if l.SSHCertificateCredentialLibrary == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil embedded l") + } + if l.StoreId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no store id") + } + if l.VaultPath == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no vault path") + } + if l.PublicId != "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + if projectId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no project id") + } + if l.Username == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no username") + } + + l = l.clone() + + if l.KeyType == "" { + l.KeyType = KeyTypeEd25519 + } + + if l.KeyBits == KeyBitsDefault { + l.KeyBits = l.getDefaultKeyBits() + } + + if l.GetCredentialType() == "" { + l.SSHCertificateCredentialLibrary.CredentialType = string(credential.SshCertificateType) + } + if l.GetCredentialType() != string(credential.SshCertificateType) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid credential type") + } + + id, err := newSSHCertificateCredentialLibraryId() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + l.setId(id) + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var newSSHCertificateCredentialLibrary *SSHCertificateCredentialLibrary + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + var msgs []*oplog.Message + ticket, err := w.GetTicket(ctx, l) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) + } + + // insert credential library + newSSHCertificateCredentialLibrary = l.clone() + var lOplogMsg oplog.Message + if err := w.Create(ctx, newSSHCertificateCredentialLibrary, db.NewOplogMsg(&lOplogMsg)); err != nil { + return errors.Wrap(ctx, err, op) + } + msgs = append(msgs, &lOplogMsg) + + metadata := l.oplog(oplog.OpType_OP_TYPE_CREATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in credential store: %s: name %s already exists", l.StoreId, l.Name))) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in credential store: %s", l.StoreId))) + } + return newSSHCertificateCredentialLibrary, nil +} + +// UpdateSSHCertificateCredentialLibrary updates the repository entry for l.PublicId with +// the values in l for the fields listed in fieldMaskPaths. It returns a +// new SSHCertificateCredentialLibrary containing the updated values and a count of the +// number of records updated. l is not changed. +// +// l must contain a valid PublicId. Name, Description, VaultPath, Username, +// KeyType, KeyBits, Ttl, KeyId, CriticalOptions, and Extensions can be updated. If +// l.Name is set to a non-empty string, it must be unique within l.StoreId. +// +// An attribute of l will be set to NULL in the database if the attribute +// in l is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdateSSHCertificateCredentialLibrary(ctx context.Context, projectId string, l *SSHCertificateCredentialLibrary, version uint32, fieldMaskPaths []string, _ ...Option) (*SSHCertificateCredentialLibrary, int, error) { + const op = "vault.(Repository).UpdateSSHCertificateCredentialLibrary" + if l == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing SSHCertificateCredentialLibrary") + } + if l.SSHCertificateCredentialLibrary == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded SSHCertificateCredentialLibrary") + } + if l.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + if version == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + } + if projectId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + l = l.clone() + + var keyTypeChange, keyBitChangeDefault bool + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(nameField, f): + case strings.EqualFold(descriptionField, f): + case strings.EqualFold(vaultPathField, f): + case strings.EqualFold(usernameField, f): + case strings.EqualFold(keyTypeField, f): + keyTypeChange = true + case strings.EqualFold(keyBitsField, f): + keyBitChangeDefault = l.KeyBits == KeyBitsDefault + case strings.EqualFold(ttlField, f): + case strings.EqualFold(keyIdField, f): + case strings.EqualFold(CriticalOptionsField, f): + case strings.EqualFold(ExtensionsField, f): + + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) + } + } + + if keyTypeChange && l.KeyType == "" { + l.KeyType = KeyTypeEd25519 + } + + if keyTypeChange && keyBitChangeDefault { + l.KeyBits = l.getDefaultKeyBits() + } + + origLib, err := r.LookupSSHCertificateCredentialLibrary(ctx, l.PublicId) + switch { + case err != nil: + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) + case origLib == nil: + return nil, db.NoRowsAffected, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("credential library %s", l.PublicId)) + } + + if keyBitChangeDefault && !keyTypeChange { + l.KeyBits = origLib.getDefaultKeyBits() + } + + var dbMask, nullFields []string + dbMask, nullFields = dbw.BuildUpdatePaths( + map[string]any{ + nameField: l.Name, + descriptionField: l.Description, + vaultPathField: l.VaultPath, + usernameField: l.Username, + keyTypeField: l.KeyType, + keyBitsField: l.KeyBits, + ttlField: l.Ttl, + keyIdField: l.KeyId, + CriticalOptionsField: l.CriticalOptions, + ExtensionsField: l.Extensions, + }, + fieldMaskPaths, + []string{keyBitsField}, + ) + + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), + errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsUpdated int + var returnedCredentialLibrary *SSHCertificateCredentialLibrary + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(rr db.Reader, w db.Writer) error { + var msgs []*oplog.Message + ticket, err := w.GetTicket(ctx, l) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) + } + + l := l.clone() + var lOplogMsg oplog.Message + + // Update the credential library table + switch { + case len(dbMask) == 0 && len(nullFields) == 0: + // the credential library's fields are not being updated, + // just one of it's child objects, so we just need to + // update the library's version. + l.Version = version + 1 + rowsUpdated, err = w.Update(ctx, l, []string{"Version"}, nil, db.NewOplogMsg(&lOplogMsg), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library version")) + } + switch rowsUpdated { + case 1: + case 0: + return nil + default: + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential library version and %d rows updated", rowsUpdated)) + } + default: + rowsUpdated, err = w.Update(ctx, l, dbMask, nullFields, db.NewOplogMsg(&lOplogMsg), db.WithVersion(&version)) + if err != nil { + if errors.IsUniqueError(err) { + return errors.New(ctx, errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", l.Name, l.PublicId)) + } + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) + } + switch rowsUpdated { + case 1: + case 0: + return nil + default: + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential library and %d rows updated", rowsUpdated)) + } + } + msgs = append(msgs, &lOplogMsg) + + metadata := l.oplog(oplog.OpType_OP_TYPE_UPDATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } + + accl := allocSSHCertificateCredentialLibrary() + accl.PublicId = l.PublicId + if err = rr.LookupById(ctx, accl); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve updated credential library")) + } + returnedCredentialLibrary = accl + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", l.Name, l.PublicId)) + } + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(l.PublicId)) + } + + return returnedCredentialLibrary, rowsUpdated, nil +} + +// LookupSSHCertificateCredentialLibrary returns the SSHCertificateCredentialLibrary for publicId. +// Returns nil, nil if no SSHCertificateCredentialLibrary is found for publicId. +func (r *Repository) LookupSSHCertificateCredentialLibrary(ctx context.Context, publicId string, _ ...Option) (*SSHCertificateCredentialLibrary, error) { + const op = "vault.(Repository).LookupCredentialLibrary" + if publicId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + l := allocSSHCertificateCredentialLibrary() + l.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, l); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", publicId))) + } + return l, nil +} + +// ListSSHCertificateCredentialLibraries returns a slice of SSHCertificateCredentialLibraries for the +// storeId. WithLimit is the only option supported. +func (r *Repository) ListSSHCertificateCredentialLibraries(ctx context.Context, storeId string, opt ...Option) ([]*SSHCertificateCredentialLibrary, error) { + const op = "vault.(Repository).ListSSHCertificateCredentialLibraries" + if storeId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no storeId") + } + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var libs []*SSHCertificateCredentialLibrary + err := r.reader.SearchWhere(ctx, &libs, "store_id = ?", []any{storeId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return libs, nil +} + +// DeleteSSHCertificateCredentialLibrary deletes publicId from the repository and returns +// the number of records deleted. +func (r *Repository) DeleteSSHCertificateCredentialLibrary(ctx context.Context, projectId string, publicId string, _ ...Option) (int, error) { + const op = "vault.(Repository).DeleteSSHCertificateCredentialLibrary" + if publicId == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + if projectId == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no project id") + } + + l := allocSSHCertificateCredentialLibrary() + l.PublicId = publicId + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + dl := l.clone() + rowsDeleted, err = w.Delete(ctx, dl, db.WithOplog(oplogWrapper, l.oplog(oplog.OpType_OP_TYPE_DELETE))) + if err == nil && rowsDeleted > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 CredentialLibrary would have been deleted") + } + return err + }, + ) + + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("delete failed for %s", l.PublicId))) + } + + return rowsDeleted, nil +} diff --git a/internal/credential/vault/repository_ssh_certificate_credential_library_test.go b/internal/credential/vault/repository_ssh_certificate_credential_library_test.go new file mode 100644 index 000000000..55898d7b3 --- /dev/null +++ b/internal/credential/vault/repository_ssh_certificate_credential_library_test.go @@ -0,0 +1,1948 @@ +package vault + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault/store" + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/scheduler" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_CreateSSHCertificateCredentialLibrary(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + + tests := []struct { + name string + in *SSHCertificateCredentialLibrary + opts []Option + want *SSHCertificateCredentialLibrary + wantErr errors.Code + }{ + { + name: "nil-SSHCertificateCredentialLibrary", + wantErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-SSHCertificateCredentialLibrary", + in: &SSHCertificateCredentialLibrary{}, + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-no-store-id", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary("", "/ssh/sign/foo", "name") + return s + }(), + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-public-id-set", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + ) + s.PublicId = "abcd_OOOOOOOOOO" + return s + }(), + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-no-vault-path", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "", + "name", + ) + return s + }(), + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-vault-path-invalid-endpoint", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/not-sign/foo", + "name", + ) + return s + }(), + wantErr: errors.CheckConstraint, + }, + { + name: "invalid-no-username", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "", + ) + return s + }(), + wantErr: errors.InvalidParameter, + }, + { + name: "valid-no-options", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + }, + }, + }, + { + name: "valid-with-name", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "test-name-repo", + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + }, + }, + }, + { + name: "valid-with-description", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithDescription("test-description-repo"), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + Description: "test-description-repo", + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + }, + }, + }, + { + name: "valid-key-type-ed25519", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsDefault), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: KeyBitsDefault, + }, + }, + }, + { + name: "valid-key-type-ecdsa-0", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsDefault), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa256, + }, + }, + }, + { + name: "valid-key-type-ecdsa-256", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa256, + }, + }, + }, + { + name: "valid-key-type-ecdsa-384", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa384), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa384, + }, + }, + }, + { + name: "valid-key-type-ecdsa-521", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa521), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa521, + }, + }, + }, + { + name: "valid-key-type-rsa-0", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsDefault), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa2048, + }, + }, + }, + { + name: "valid-key-type-rsa-2048", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsRsa2048), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa2048, + }, + }, + }, + { + name: "valid-key-type-rsa-3072", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsRsa3072), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa3072, + }, + }, + }, + { + name: "valid-key-type-rsa-4096", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsRsa4096), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa4096, + }, + }, + }, + { + name: "invalid-key-type-key-bits-ecdsa-2048", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsRsa2048), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ecdsa-3072", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsRsa3072), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ecdsa-4096", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsRsa4096), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-rsa-256", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-rsa-384", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsEcdsa384), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-rsa-521", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsEcdsa521), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-256", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-384", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsEcdsa384), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-521", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsEcdsa521), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-2048", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsRsa2048), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-3072", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsRsa3072), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-4096", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsRsa4096), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "valid-critical-options", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithCriticalOptions(`{"force-command": "/bin/foo"}`), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + CriticalOptions: `{"force-command": "/bin/foo"}`, + }, + }, + }, + { + name: "valid-extensions", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithExtensions(`{"permit-X11-forwarding": "", "permit-pty": ""}`), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + Extensions: `{"permit-X11-forwarding": "", "permit-pty": ""}`, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + require.NoError(err) + require.NotNil(repo) + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), tt.in, tt.opts...) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assert.Empty(tt.in.PublicId) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.NotSame(tt.in, got) + assert.Equal(tt.want.Name, got.Name) + assert.Equal(tt.want.Description, got.Description) + assert.Equal(tt.want.KeyType, got.KeyType) + assert.Equal(tt.want.KeyBits, got.KeyBits) + assert.Equal(tt.want.KeyId, got.KeyId) + assert.Equal(tt.want.Ttl, got.Ttl) + assert.Equal(tt.want.CriticalOptions, got.CriticalOptions) + assert.Equal(tt.want.Extensions, got.Extensions) + assert.Equal(got.CredentialType(), credential.SshCertificateType) + assert.Equal(got.CreateTime, got.UpdateTime) + + assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } + + t.Run("invalid-duplicate-names", 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) + require.NoError(err) + require.NotNil(repo) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + KeyType: KeyTypeEd25519, + VaultPath: "/ssh/sign/foo", + Name: "test-name-repo", + Username: "name", + }, + } + + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + got2, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err: %q got: %q", errors.NotUnique, err) + assert.Nil(got2) + }) + + t.Run("valid-duplicate-names-diff-stores", 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) + require.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + + csA, csB := css[0], css[1] + + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeEd25519, + VaultPath: "/ssh/sign/foo", + Name: "test-name-repo", + Username: "name", + }, + } + in2 := in.clone() + + in.StoreId = csA.GetPublicId() + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + in2.StoreId = csB.GetPublicId() + got2, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in2) + require.NoError(err) + require.NotNil(got2) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got2.GetPublicId()) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.Name) + assert.Equal(in2.Description, got2.Description) + assert.Equal(got2.CreateTime, got2.UpdateTime) + }) +} + +func TestRepository_LookupSSHCertificateCredentialLibrary(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + { + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + + cs := css[0] + csWithExpiredToken := css[1] + rows, err := rw.Exec(context.Background(), + "update credential_vault_token set status = ? where token_hmac = ?", + []any{ExpiredToken, csWithExpiredToken.Token().TokenHmac}) + require.NoError(t, err) + require.Equal(t, 1, rows) + + tests := []struct { + name string + in *SSHCertificateCredentialLibrary + }{ + { + name: "valid-no-options", + in: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + }, + + { + name: "valid-with-expired-cred-store-token", + in: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: csWithExpiredToken.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + }, + { + name: "valid-ssh-certificate-credential-type", + in: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + CredentialType: string(credential.SshCertificateType), + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + orig, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), tt.in) + assert.NoError(err) + require.NotEmpty(orig) + // test + got, err := repo.LookupSSHCertificateCredentialLibrary(ctx, orig.GetPublicId()) + assert.NoError(err) + require.NotEmpty(got) + assert.Equal(orig.Name, got.Name) + assert.Equal(orig.Description, got.Description) + assert.Equal(orig.CredentialType(), got.CredentialType()) + }) + } + } + + t.Run("empty-public-id", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + // test + got, err := repo.LookupCredentialLibrary(ctx, "") + wantErr := errors.InvalidParameter + assert.Truef(errors.Match(errors.T(wantErr), err), "want err: %q got: %q", wantErr, err) + assert.Nil(got) + }) + + t.Run("not-found", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + badId, err := newSSHCertificateCredentialLibraryId() + assert.NoError(err) + require.NotNil(badId) + // test + got, err := repo.LookupCredentialLibrary(ctx, badId) + assert.NoError(err) + assert.Empty(got) + }) +} + +func TestRepository_ListSSHCertificateCredentialLibraries_Limits(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + sche := scheduler.TestScheduler(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + const count = 10 + libs := TestSSHCertificateCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), count) + + tests := []struct { + name string + repoOpts []Option + listOpts []Option + wantLen int + }{ + { + name: "with-no-limits", + wantLen: count, + }, + { + name: "with-repo-limit", + repoOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "with-negative-repo-limit", + repoOpts: []Option{WithLimit(-1)}, + wantLen: count, + }, + { + name: "with-list-limit", + listOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "with-negative-list-limit", + listOpts: []Option{WithLimit(-1)}, + wantLen: count, + }, + { + name: "with-repo-smaller-than-list-limit", + repoOpts: []Option{WithLimit(2)}, + listOpts: []Option{WithLimit(6)}, + wantLen: 6, + }, + { + name: "with-repo-larger-than-list-limit", + repoOpts: []Option{WithLimit(6)}, + listOpts: []Option{WithLimit(2)}, + wantLen: 2, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche, tt.repoOpts...) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListSSHCertificateCredentialLibraries(ctx, libs[0].StoreId, tt.listOpts...) + require.NoError(err) + assert.Len(got, tt.wantLen) + }) + } +} + +func TestRepository_UpdateSSHCertificateCredentialLibrary(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + changeVaultPath := func(p string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.VaultPath = p + return l + } + } + + changeName := func(n string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Name = n + return l + } + } + + changeDescription := func(d string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Description = d + return l + } + } + + changeUsername := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Username = s + return l + } + } + + changeKeyType := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.KeyType = s + return l + } + } + + changeKeyBits := func(b uint32) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.KeyBits = b + return l + } + } + + changeTtl := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Ttl = s + return l + } + } + + changeKeyId := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.KeyId = s + return l + } + } + + changeCriticalOptions := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.CriticalOptions = s + return l + } + } + + changeExtensions := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Extensions = s + return l + } + } + + makeNil := func() func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return nil + } + } + + makeEmbeddedNil := func() func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return &SSHCertificateCredentialLibrary{} + } + } + + deletePublicId := func() func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.PublicId = "" + return l + } + } + + nonExistentPublicId := func() func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.PublicId = "abcd_OOOOOOOOOO" + return l + } + } + + combine := func(fns ...func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + for _, fn := range fns { + l = fn(l) + } + return l + } + } + + tests := []struct { + name string + origFn func(string) *SSHCertificateCredentialLibrary + chgFn func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary + masks []string + want *SSHCertificateCredentialLibrary + wantCount int + wantErr errors.Code + }{ + { + name: "nil-credential-library", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: makeNil(), + masks: []string{nameField, descriptionField}, + wantErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-credential-library", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: makeEmbeddedNil(), + masks: []string{nameField, descriptionField}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-public-id", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: deletePublicId(), + masks: []string{nameField, descriptionField}, + wantErr: errors.InvalidPublicId, + }, + { + name: "updating-non-existent-credential-library", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: combine(nonExistentPublicId(), changeName("test-update-name-repo")), + masks: []string{nameField}, + wantErr: errors.RecordNotFound, + }, + { + name: "empty-field-mask", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: changeName("test-update-name-repo"), + wantErr: errors.EmptyFieldMask, + }, + { + name: "read-only-fields-in-field-mask", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "StoreId"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "unknown-field-in-field-mask", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Bilbo"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "change-name", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{nameField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-update-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-description", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithDescription("test-description-repo"), + ) + return s + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{descriptionField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")), + masks: []string{nameField, descriptionField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-update-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-username", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: changeUsername("update-name"), + masks: []string{usernameField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "update-name", + }, + }, + wantCount: 1, + }, + { + name: "change-keytype", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + ) + return s + }, + chgFn: combine(changeKeyType(KeyTypeEcdsa), changeKeyBits(KeyBitsDefault)), + masks: []string{keyTypeField, keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa256, + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-keybits", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }, + chgFn: changeKeyBits(KeyBitsEcdsa384), + masks: []string{keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa384, + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-keybits-to-default-ecdsa", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa384), + ) + return s + }, + chgFn: changeKeyBits(KeyBitsDefault), + masks: []string{keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa256, + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-keybits-to-default-rsa", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsRsa3072), + ) + return s + }, + chgFn: changeKeyBits(KeyBitsDefault), + masks: []string{keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa2048, + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-ttl", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithTtl("10s"), + ) + return s + }, + chgFn: changeTtl("1h"), + masks: []string{ttlField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Ttl: "1h", + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-keyid", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyId("id"), + ) + return s + }, + chgFn: changeKeyId("update-id"), + masks: []string{keyIdField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyId: "update-id", + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-critical-options", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithCriticalOptions("options"), + ) + return s + }, + chgFn: changeCriticalOptions("update-options"), + masks: []string{CriticalOptionsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + CriticalOptions: "update-options", + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-extensions", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithExtensions("extensions"), + ) + return s + }, + chgFn: changeExtensions("update-extensions"), + masks: []string{ExtensionsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Extensions: "update-extensions", + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + masks: []string{nameField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + masks: []string{descriptionField}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + masks: []string{descriptionField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + masks: []string{nameField}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-update-name-repo", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-vault-path", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: changeVaultPath("/ssh/issue/foo"), + masks: []string{vaultPathField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/issue/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-vault-path-invalid", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: changeVaultPath("/ssh/not-sign/foo"), + masks: []string{vaultPathField}, + wantErr: errors.CheckConstraint, + }, + { + name: "delete-vault-path", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: changeVaultPath(""), + masks: []string{vaultPathField}, + wantErr: errors.NotNull, + }, + { + name: "change-key-type-invalid", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }, + chgFn: changeKeyType(KeyTypeEd25519), + masks: []string{keyTypeField}, + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "change-key-type-bits-valid", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }, + chgFn: combine(changeKeyType(KeyTypeRsa), changeKeyBits(KeyBitsRsa2048)), + masks: []string{keyTypeField, keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa2048, + }, + }, + wantCount: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + + ttOrig := tt.origFn(cs.GetPublicId()) + orig, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), ttOrig) + assert.NoError(err) + require.NotNil(orig) + + if tt.chgFn != nil { + orig = tt.chgFn(orig) + } + got, gotCount, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), orig, 1, tt.masks) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + require.NoError(err) + assert.Empty(ttOrig.PublicId) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.NotSame(ttOrig, got) + assert.Equal(ttOrig.StoreId, got.StoreId) + underlyingDB, err := conn.SqlDB(ctx) + require.NoError(err) + dbassert := dbassert.New(t, underlyingDB) + + switch tt.want.Name { + case "": + dbassert.IsNull(got, "name") + default: + assert.Equal(tt.want.Name, got.Name) + } + + switch tt.want.Description { + case "": + dbassert.IsNull(got, "description") + default: + assert.Equal(tt.want.Description, got.Description) + } + + switch tt.want.Username { + case "": + dbassert.IsNull(got, "Username") + default: + assert.Equal(tt.want.Username, got.Username) + } + + switch tt.want.KeyType { + case "": + assert.Equal(got.KeyType, KeyTypeEd25519) + default: + assert.Equal(tt.want.KeyType, got.KeyType) + } + + switch tt.want.KeyBits { + case (0): + assert.Zero(got.KeyBits) + default: + assert.Equal(tt.want.KeyBits, got.KeyBits) + } + + switch tt.want.Ttl { + case "": + dbassert.IsNull(got, "Ttl") + default: + assert.Equal(tt.want.Ttl, got.Ttl) + } + + switch tt.want.KeyId { + case "": + dbassert.IsNull(got, "KeyId") + default: + assert.Equal(tt.want.KeyId, got.KeyId) + } + + switch tt.want.CriticalOptions { + case "": + dbassert.IsNull(got, "CriticalOptions") + default: + assert.Equal(tt.want.CriticalOptions, got.CriticalOptions) + } + + switch tt.want.Extensions { + case "": + dbassert.IsNull(got, "Extensions") + default: + assert.Equal(tt.want.Extensions, got.Extensions) + } + + if tt.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } + + t.Run("invalid-duplicate-names", 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) + + name := "test-dup-name" + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + libs := TestSSHCertificateCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 2) + + lA, lB := libs[0], libs[1] + + lA.Name = name + got1, gotCount1, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), lA, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(got1) + assert.Equal(name, got1.Name) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, lA.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + + lB.Name = name + got2, gotCount2, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), lB, 1, []string{"name"}) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err code: %v got err: %v", errors.NotUnique, err) + assert.Nil(got2) + assert.Equal(db.NoRowsAffected, gotCount2, "row count") + err = db.TestVerifyOplog(t, rw, lB.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.IsNotFoundError(err)) + }) + + t.Run("valid-duplicate-names-diff-CredentialStores", 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(), 2) + + csA, csB := css[0], css[1] + + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Username: "name", + VaultPath: "/ssh/sign/foo", + Name: "test-name-repo", + }, + } + in2 := in.clone() + + in.StoreId = csA.GetPublicId() + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + assert.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + + in2.StoreId = csB.GetPublicId() + in2.Name = "first-name" + got2, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in2) + assert.NoError(err) + require.NotNil(got2) + got2.Name = got.Name + got3, gotCount3, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), got2, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(got3) + assert.NotSame(got2, got3) + assert.Equal(got.Name, got3.Name) + assert.Equal(got2.Description, got3.Description) + assert.Equal(1, gotCount3, "row count") + 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 := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Username: "name", + VaultPath: "/ssh/sign/foo", + Name: "test-name-repo", + }, + } + + in.StoreId = cs.GetPublicId() + got, err := repo.CreateSSHCertificateCredentialLibrary(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 = ?", + []any{ExpiredToken, cs.Token().TokenHmac}) + require.NoError(err) + require.Equal(1, rows) + + got.Name = "new-name" + updated, gotCount, err := repo.UpdateSSHCertificateCredentialLibrary(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() + 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(), 2) + + csA, csB := css[0], css[1] + + lA := TestSSHCertificateCredentialLibraries(t, conn, wrapper, csA.GetPublicId(), 1)[0] + lB := TestSSHCertificateCredentialLibraries(t, conn, wrapper, csB.GetPublicId(), 1)[0] + + assert.NotEqual(lA.StoreId, lB.StoreId) + orig := lA.clone() + + lA.StoreId = lB.StoreId + assert.Equal(lA.StoreId, lB.StoreId) + + got1, gotCount1, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), lA, 1, []string{"name"}) + + assert.NoError(err) + require.NotNil(got1) + assert.Equal(orig.StoreId, got1.StoreId) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, lA.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) + + t.Run("change-key-type-bits-consecutive", 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)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Username: "name", + VaultPath: "/ssh/sign/foo", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa521, + }, + } + in.StoreId = cs.GetPublicId() + + orig, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + assert.NoError(err) + require.NotNil(orig) + + orig.KeyBits = KeyBitsEcdsa256 + + got, gotCount, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), orig, 1, []string{keyBitsField}) + require.NoError(err) + assert.Empty(in.PublicId) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.Equal(1, gotCount, "row count") + assert.NotSame(orig, got) + assert.Equal(orig.StoreId, got.StoreId) + + orig.KeyType = KeyTypeRsa + orig.KeyBits = KeyBitsDefault + + got, gotCount, err = repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), orig, 2, []string{keyTypeField, keyBitsField}) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.Equal(1, gotCount, "row count") + assert.NotSame(orig, got) + assert.Equal(orig.StoreId, got.StoreId) + + orig.KeyBits = KeyBitsRsa3072 + + got, gotCount, err = repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), orig, 3, []string{keyTypeField}) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.Equal(1, gotCount, "row count") + assert.NotSame(orig, got) + assert.Equal(orig.StoreId, got.StoreId) + + assert.Equal(KeyTypeRsa, orig.KeyType) + assert.Equal(uint32(KeyBitsRsa3072), orig.KeyBits) + }) +} + +func TestRepository_DeleteSSHCertificateCredentialLibrary(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + { + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + l := TestSSHCertificateCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 1)[0] + + badId, err := newSSHCertificateCredentialLibraryId() + require.NoError(t, err) + require.NotNil(t, badId) + + tests := []struct { + name string + in string + want int + wantErr errors.Code + }{ + { + name: "found", + in: l.GetPublicId(), + want: 1, + }, + { + name: "not-found", + in: badId, + }, + { + name: "empty-public-id", + in: "", + wantErr: errors.InvalidParameter, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + got, err := repo.DeleteSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), tt.in) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + return + } + assert.NoError(err) + assert.Equal(tt.want, got, "row count") + + cl, err := repo.LookupSSHCertificateCredentialLibrary(ctx, tt.in) + assert.Empty(err) + assert.Empty(cl) + }) + } + } +} diff --git a/internal/credential/vault/setup_manual_test.go b/internal/credential/vault/setup_manual_test.go index aa4fff3fc..def2593c9 100644 --- a/internal/credential/vault/setup_manual_test.go +++ b/internal/credential/vault/setup_manual_test.go @@ -74,7 +74,7 @@ func TestSetupSleepyDevEnvironment(t *testing.T) { require.NoError(t, err) require.NotNil(t, store) libClient := credentiallibraries.NewClient(client) - lib, err := libClient.Create(ctx, store.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath(path.Join("database", "creds", "opened")), + lib, err := libClient.Create(ctx, store.Item.Type, store.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath(path.Join("database", "creds", "opened")), credentiallibraries.WithVaultCredentialLibraryHttpMethod("GET"), ) require.NoError(t, err) diff --git a/internal/credential/vault/ssh_certificate_credential_library.go b/internal/credential/vault/ssh_certificate_credential_library.go new file mode 100644 index 000000000..9d78bf5c4 --- /dev/null +++ b/internal/credential/vault/ssh_certificate_credential_library.go @@ -0,0 +1,120 @@ +package vault + +import ( + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault/store" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" +) + +const ( + KeyTypeEcdsa = "ecdsa" + KeyTypeEd25519 = "ed25519" + KeyTypeRsa = "rsa" + + KeyBitsDefault = 0 + + KeyBitsEcdsa256 = 256 + KeyBitsEcdsa384 = 384 + KeyBitsEcdsa521 = 521 + + KeyBitsRsa2048 = 2048 + KeyBitsRsa3072 = 3072 + KeyBitsRsa4096 = 4096 +) + +// SSHCertificateCredentialLibrary is a credential library that issues +// ssh certificate using the vault ssh secret engine. +// See: https://developer.hashicorp.com/vault/api-docs/secret/ssh#sign-ssh-key +type SSHCertificateCredentialLibrary struct { + *store.SSHCertificateCredentialLibrary + tableName string `gorm:"-"` +} + +// NewSSHCertificateCredentialLibrary creates a new in memory SSHCertificateCredentialLibrary +// for a Vault backend at vaultPath assigned to storeId. The SSH username field must be set. +// Name, description, key type, key bits, ttl, key id, critical options, and extensions +// are the only valid options. All other options are ignored. +func NewSSHCertificateCredentialLibrary(storeId string, vaultPath string, username string, opt ...Option) (*SSHCertificateCredentialLibrary, error) { + const op = "vault.NewSSHCertificateCredentialLibrary" + opts := getOpts(opt...) + + l := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: storeId, + Name: opts.withName, + Description: opts.withDescription, + VaultPath: vaultPath, + Username: username, + KeyType: opts.withKeyType, + KeyBits: opts.withKeyBits, + Ttl: opts.withTtl, + KeyId: opts.withKeyId, + CriticalOptions: opts.withCriticalOptions, + Extensions: opts.withExtensions, + CredentialType: string(credential.SshCertificateType), + }, + } + + return l, nil +} + +func allocSSHCertificateCredentialLibrary() *SSHCertificateCredentialLibrary { + return &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{}, + } +} + +func (l *SSHCertificateCredentialLibrary) clone() *SSHCertificateCredentialLibrary { + cp := proto.Clone(l.SSHCertificateCredentialLibrary) + return &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: cp.(*store.SSHCertificateCredentialLibrary), + } +} + +func (l *SSHCertificateCredentialLibrary) setId(i string) { + l.PublicId = i +} + +// TableName returns the table name. +func (l *SSHCertificateCredentialLibrary) TableName() string { + if l.tableName != "" { + return l.tableName + } + return "credential_vault_ssh_cert_library" +} + +// SetTableName sets the table name. +func (l *SSHCertificateCredentialLibrary) SetTableName(n string) { + l.tableName = n +} + +func (l *SSHCertificateCredentialLibrary) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{l.PublicId}, + "resource-type": []string{"credential-vault-ssh-cert-library"}, + "op-type": []string{op.String()}, + } + if l.StoreId != "" { + metadata["store-id"] = []string{l.StoreId} + } + return metadata +} + +func (l *SSHCertificateCredentialLibrary) getDefaultKeyBits() uint32 { + switch l.KeyType { + case KeyTypeEcdsa: + return KeyBitsEcdsa256 + case KeyTypeRsa: + return KeyBitsRsa2048 + default: + return KeyBitsDefault + } +} + +// CredentialType returns the type of credential the library retrieves. +func (l *SSHCertificateCredentialLibrary) CredentialType() credential.Type { + return credential.Type(l.SSHCertificateCredentialLibrary.CredentialType) +} + +var _ credential.Library = (*SSHCertificateCredentialLibrary)(nil) diff --git a/internal/credential/vault/store/vault.pb.go b/internal/credential/vault/store/vault.pb.go index 7a198ac30..06bd6a14e 100644 --- a/internal/credential/vault/store/vault.pb.go +++ b/internal/credential/vault/store/vault.pb.go @@ -629,6 +629,210 @@ func (x *CredentialLibrary) GetCredentialType() string { return "" } +type SSHCertificateCredentialLibrary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within project_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // store_id of the owning vault credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + StoreId string `protobuf:"bytes,6,opt,name=store_id,json=storeId,proto3" json:"store_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // vault_path is the path in Vault to request credentials from. + // It must be set. + // @inject_tag: `gorm:"not_null"` + VaultPath string `protobuf:"bytes,8,opt,name=vault_path,json=vaultPath,proto3" json:"vault_path,omitempty" gorm:"not_null"` + // username is the username to use when making an SSH connection. + // It must be set. + // @inject_tag: `gorm:"not_null"` + Username string `protobuf:"bytes,9,opt,name=username,proto3" json:"username,omitempty" gorm:"not_null"` + // key_type specifies the key type to use when generating an SSH private key. + // Values must be "rsa", "ed25519", or "ecdsa". + // @inject_tag: `gorm:"not_null"` + KeyType string `protobuf:"bytes,10,opt,name=key_type,json=keyType,proto3" json:"key_type,omitempty" gorm:"not_null"` + // key_bits specifies the number of bits to use to generate an SSH private key. + // Not used if key_type is ed25519. + // @inject_tag: `gorm:"not_null"` + KeyBits uint32 `protobuf:"varint,11,opt,name=key_bits,json=keyBits,proto3" json:"key_bits,omitempty" gorm:"not_null"` + // ttl specifies the requested time to live for the certificate. + // @inject_tag: `gorm:"default:null"` + Ttl string `protobuf:"bytes,12,opt,name=ttl,proto3" json:"ttl,omitempty" gorm:"default:null"` + // key_id specifies the key id that the created certificate should have. + // @inject_tag: `gorm:"default:null"` + KeyId string `protobuf:"bytes,13,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"default:null"` + // critical_options specifies a map of the critical options that the certificate should be signed for. + // @inject_tag: `gorm:"default:null"` + CriticalOptions string `protobuf:"bytes,14,opt,name=critical_options,json=criticalOptions,proto3" json:"critical_options,omitempty" gorm:"default:null"` + // extensions specifies a map of the extensions that the certificate should be signed for. + // @inject_tag: `gorm:"default:null"` + Extensions string `protobuf:"bytes,15,opt,name=extensions,proto3" json:"extensions,omitempty" gorm:"default:null"` + // credential_type is always ssh_certificate + // @inject_tag: `gorm:"default:null"` + CredentialType string `protobuf:"bytes,16,opt,name=credential_type,json=credentialType,proto3" json:"credential_type,omitempty" gorm:"default:null"` +} + +func (x *SSHCertificateCredentialLibrary) Reset() { + *x = SSHCertificateCredentialLibrary{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SSHCertificateCredentialLibrary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SSHCertificateCredentialLibrary) ProtoMessage() {} + +func (x *SSHCertificateCredentialLibrary) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SSHCertificateCredentialLibrary.ProtoReflect.Descriptor instead. +func (*SSHCertificateCredentialLibrary) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{4} +} + +func (x *SSHCertificateCredentialLibrary) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *SSHCertificateCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *SSHCertificateCredentialLibrary) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetStoreId() string { + if x != nil { + return x.StoreId + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *SSHCertificateCredentialLibrary) GetVaultPath() string { + if x != nil { + return x.VaultPath + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetKeyType() string { + if x != nil { + return x.KeyType + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetKeyBits() uint32 { + if x != nil { + return x.KeyBits + } + return 0 +} + +func (x *SSHCertificateCredentialLibrary) GetTtl() string { + if x != nil { + return x.Ttl + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetCriticalOptions() string { + if x != nil { + return x.CriticalOptions + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetExtensions() string { + if x != nil { + return x.Extensions + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetCredentialType() string { + if x != nil { + return x.CredentialType + } + return "" +} + type Credential struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -688,7 +892,7 @@ type Credential struct { func (x *Credential) Reset() { *x = Credential{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -701,7 +905,7 @@ func (x *Credential) String() string { func (*Credential) ProtoMessage() {} func (x *Credential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -714,7 +918,7 @@ func (x *Credential) ProtoReflect() protoreflect.Message { // Deprecated: Use Credential.ProtoReflect.Descriptor instead. func (*Credential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{4} + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{5} } func (x *Credential) GetPublicId() string { @@ -832,7 +1036,7 @@ type UsernamePasswordOverride struct { func (x *UsernamePasswordOverride) Reset() { *x = UsernamePasswordOverride{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -845,7 +1049,7 @@ func (x *UsernamePasswordOverride) String() string { func (*UsernamePasswordOverride) ProtoMessage() {} func (x *UsernamePasswordOverride) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -858,7 +1062,7 @@ func (x *UsernamePasswordOverride) ProtoReflect() protoreflect.Message { // Deprecated: Use UsernamePasswordOverride.ProtoReflect.Descriptor instead. func (*UsernamePasswordOverride) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{5} + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{6} } func (x *UsernamePasswordOverride) GetLibraryId() string { @@ -923,7 +1127,7 @@ type SshPrivateKeyOverride struct { func (x *SshPrivateKeyOverride) Reset() { *x = SshPrivateKeyOverride{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -936,7 +1140,7 @@ func (x *SshPrivateKeyOverride) String() string { func (*SshPrivateKeyOverride) ProtoMessage() {} func (x *SshPrivateKeyOverride) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -949,7 +1153,7 @@ func (x *SshPrivateKeyOverride) ProtoReflect() protoreflect.Message { // Deprecated: Use SshPrivateKeyOverride.ProtoReflect.Descriptor instead. func (*SshPrivateKeyOverride) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{6} + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{7} } func (x *SshPrivateKeyOverride) GetLibraryId() string { @@ -1146,72 +1350,131 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []by 0x0f, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x22, 0xc3, 0x04, 0x0a, 0x0a, 0x43, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, - 0x72, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x68, 0x6d, 0x61, - 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x48, 0x6d, - 0x61, 0x63, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, + 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x22, 0xaa, 0x07, 0x0a, 0x1f, 0x53, 0x53, + 0x48, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, + 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x3f, 0x0a, 0x0a, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x20, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x09, 0x56, 0x61, 0x75, + 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x70, 0x61, 0x74, 0x68, 0x52, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, + 0x74, 0x68, 0x12, 0x3f, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x23, 0xc2, 0xdd, 0x29, 0x1f, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x07, 0x4b, 0x65, 0x79, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x0d, 0x42, 0x22, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x42, + 0x69, 0x74, 0x73, 0x12, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, + 0x6b, 0x65, 0x79, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x42, 0x69, 0x74, + 0x73, 0x12, 0x2b, 0x0a, 0x03, 0x74, 0x74, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x19, + 0xc2, 0xdd, 0x29, 0x15, 0x0a, 0x03, 0x54, 0x74, 0x6c, 0x12, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x74, 0x74, 0x6c, 0x52, 0x03, 0x74, 0x74, 0x6c, 0x12, 0x35, + 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, + 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x05, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x11, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x52, 0x05, + 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x5d, 0x0a, 0x10, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, + 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x32, 0xc2, 0xdd, 0x29, 0x2e, 0x0a, 0x0f, 0x43, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x0f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x47, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xc2, 0xdd, 0x29, 0x23, 0x0a, 0x0a, + 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x15, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x0a, + 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x22, 0xc3, 0x04, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x48, 0x6d, 0x61, 0x63, 0x12, + 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x56, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, - 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, - 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x53, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, - 0x61, 0x62, 0x6c, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x52, 0x65, - 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, - 0x97, 0x01, 0x0a, 0x18, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, - 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x70, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xe2, 0x01, 0x0a, 0x15, 0x53, 0x73, - 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x4f, 0x76, 0x65, 0x72, 0x72, - 0x69, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, - 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, - 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, - 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x13, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x47, 0x0a, 0x20, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x1d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, - 0x68, 0x72, 0x61, 0x73, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x42, 0x45, - 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, + 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x49, 0x64, 0x12, 0x56, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x6e, + 0x65, 0x77, 0x61, 0x6c, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x6c, 0x61, 0x73, + 0x74, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x53, 0x0a, 0x0f, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, + 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x52, 0x65, 0x6e, 0x65, 0x77, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x97, 0x01, 0x0a, + 0x18, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, + 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, + 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xe2, 0x01, 0x0a, 0x15, 0x53, 0x73, 0x68, 0x50, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, + 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, + 0x2d, 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x32, + 0x0a, 0x15, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x12, 0x47, 0x0a, 0x20, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, + 0x73, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x42, 0x45, 0x5a, 0x43, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1226,36 +1489,39 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP() return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescData } -var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_controller_storage_credential_vault_store_v1_vault_proto_goTypes = []interface{}{ - (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore - (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token - (*ClientCertificate)(nil), // 2: controller.storage.credential.vault.store.v1.ClientCertificate - (*CredentialLibrary)(nil), // 3: controller.storage.credential.vault.store.v1.CredentialLibrary - (*Credential)(nil), // 4: controller.storage.credential.vault.store.v1.Credential - (*UsernamePasswordOverride)(nil), // 5: controller.storage.credential.vault.store.v1.UsernamePasswordOverride - (*SshPrivateKeyOverride)(nil), // 6: controller.storage.credential.vault.store.v1.SshPrivateKeyOverride - (*timestamp.Timestamp)(nil), // 7: controller.storage.timestamp.v1.Timestamp + (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore + (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token + (*ClientCertificate)(nil), // 2: controller.storage.credential.vault.store.v1.ClientCertificate + (*CredentialLibrary)(nil), // 3: controller.storage.credential.vault.store.v1.CredentialLibrary + (*SSHCertificateCredentialLibrary)(nil), // 4: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary + (*Credential)(nil), // 5: controller.storage.credential.vault.store.v1.Credential + (*UsernamePasswordOverride)(nil), // 6: controller.storage.credential.vault.store.v1.UsernamePasswordOverride + (*SshPrivateKeyOverride)(nil), // 7: controller.storage.credential.vault.store.v1.SshPrivateKeyOverride + (*timestamp.Timestamp)(nil), // 8: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_vault_store_v1_vault_proto_depIdxs = []int32{ - 7, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 9: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 10: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 11: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 12: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 13, // [13:13] is the sub-list for method output_type - 13, // [13:13] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 8, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 9: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 10: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 11: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 12: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 13: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 14: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 15, // [15:15] is the sub-list for method output_type + 15, // [15:15] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_controller_storage_credential_vault_store_v1_vault_proto_init() } @@ -1313,7 +1579,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { } } file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Credential); i { + switch v := v.(*SSHCertificateCredentialLibrary); i { case 0: return &v.state case 1: @@ -1325,7 +1591,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { } } file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UsernamePasswordOverride); i { + switch v := v.(*Credential); i { case 0: return &v.state case 1: @@ -1337,6 +1603,18 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { } } file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UsernamePasswordOverride); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SshPrivateKeyOverride); i { case 0: return &v.state @@ -1355,7 +1633,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/credential/vault/testing.go b/internal/credential/vault/testing.go index 817492655..d87171f84 100644 --- a/internal/credential/vault/testing.go +++ b/internal/credential/vault/testing.go @@ -140,6 +140,38 @@ func TestCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, stor return libs } +// TestSSHCertificateCredentialLibraries creates count number of vault credential +// libraries in the provided DB with the provided store id. If any errors +// are encountered during the creation of the credential libraries, the +// test will fail. +func TestSSHCertificateCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, storeId string, count int) []*SSHCertificateCredentialLibrary { + t.Helper() + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + var libs []*SSHCertificateCredentialLibrary + + for i := 0; i < count; i++ { + lib, err := NewSSHCertificateCredentialLibrary(storeId, fmt.Sprintf("vault/path%d", i), "username", withKeyType(KeyTypeEd25519)) + assert.NoError(err) + require.NotNil(lib) + id, err := newSSHCertificateCredentialLibraryId() + assert.NoError(err) + require.NotEmpty(id) + lib.PublicId = id + + ctx := context.Background() + _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + return iw.Create(ctx, lib) + }, + ) + + require.NoError(err2) + libs = append(libs, lib) + } + return libs +} + // TestCredentials creates count number of vault credentials in the provided DB with // the provided library id and session id. If any errors are encountered // during the creation of the credentials, the test will fail. diff --git a/internal/db/schema/migrations/oss/postgres/39/01_static_ssh_private_key_creds.up.sql b/internal/db/schema/migrations/oss/postgres/39/01_static_ssh_private_key_creds.up.sql index 6b5673ad2..b19952e69 100644 --- a/internal/db/schema/migrations/oss/postgres/39/01_static_ssh_private_key_creds.up.sql +++ b/internal/db/schema/migrations/oss/postgres/39/01_static_ssh_private_key_creds.up.sql @@ -6,6 +6,7 @@ begin; -- Add new constraint that only allows known types -- This replaces the constraint defined in 36/01_credential_type_up + -- Replaced in 63/01_credential_vault_ssh_cert_library alter table credential_type_enm add constraint only_predefined_credential_types_allowed check ( diff --git a/internal/db/schema/migrations/oss/postgres/63/01_credential_vault_ssh_cert_library.up.sql b/internal/db/schema/migrations/oss/postgres/63/01_credential_vault_ssh_cert_library.up.sql new file mode 100644 index 000000000..6ec697947 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/63/01_credential_vault_ssh_cert_library.up.sql @@ -0,0 +1,170 @@ +begin; + + -- drop constraint so we can add ssh_certificate + alter table credential_type_enm + drop constraint only_predefined_credential_types_allowed; + + -- Add new constraint that only allows known types + -- This replaces the constraint defined in 39/01_static_ssh_private_key_creds_up + alter table credential_type_enm + add constraint only_predefined_credential_types_allowed + check ( + name in ( + 'unspecified', + 'username_password', + 'ssh_private_key', + 'ssh_certificate' + ) + ); + + insert into credential_type_enm (name) + values ('ssh_certificate'); + + create table credential_vault_ssh_cert_key_type_enm ( + name text primary key + constraint only_predefined_key_types_allowed + check ( + name in ( + 'ed25519', + 'ecdsa', + 'rsa' + ) + ) + ); + comment on table credential_vault_ssh_cert_key_type_enm is + 'credential_vault_ssh_cert_key_type_enm is an enumeration table for the ssh key type. '; + + insert into credential_vault_ssh_cert_key_type_enm (name) + values + ('ed25519'), + ('ecdsa'), + ('rsa'); + + create table credential_vault_ssh_cert_key_bits_enm ( + bits int primary key + constraint only_predefined_key_bits_allowed + check ( + bits in ( + 0, + 2048, + 3072, + 4096, + 256, + 384, + 521 + ) + ) + ); + comment on table credential_vault_ssh_cert_key_bits_enm is + 'credential_vault_ssh_cert_key_bits_enm is an enumeration table for the ssh key bits. '; + + insert into credential_vault_ssh_cert_key_bits_enm (bits) + values + (0), + (2048), + (3072), + (4096), + (256), + (384), + (521); + + create table credential_vault_ssh_cert_valid_key_type_key_bits ( + key_type text not null + constraint credential_vault_ssh_cert_key_type_enm_fkey + references credential_vault_ssh_cert_key_type_enm (name), + key_bits int not null + constraint credential_vault_ssh_cert_key_bits_enm_fkey + references credential_vault_ssh_cert_key_bits_enm (bits), + constraint credential_vault_ssh_cert_valid_key_type_key_bits_uq + unique(key_type, key_bits) + ); + + insert into credential_vault_ssh_cert_valid_key_type_key_bits (key_type, key_bits) + values + ('ed25519', 0), + ('ecdsa', 256), + ('ecdsa', 384), + ('ecdsa', 521), + ('rsa', 2048), + ('rsa', 3072), + ('rsa', 4096); + + create table credential_vault_ssh_cert_library ( + public_id wt_public_id primary key, + store_id wt_public_id not null + constraint credential_vault_store_fkey + references credential_vault_store (public_id) + on delete cascade + on update cascade, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + vault_path text not null + constraint vault_path_must_not_be_empty + check(length(trim(vault_path)) > 0) + constraint vault_path_must_be_sign_or_issue + check(vault_path ~ '^.+\/(sign|issue)\/[^\/\\\s]+$'), + username text not null + constraint username_must_not_be_empty + check(length(trim(username)) > 0), + key_type text not null, + key_bits int not null, + ttl text, + key_id text, + critical_options bytea, + extensions bytea, + credential_type text, + project_id wt_public_id not null, + constraint credential_vault_ssh_cert_library_store_id_name_uq + unique(store_id, name), + constraint credential_vault_ssh_cert_library_store_id_public_id_uq + unique(store_id, public_id), + constraint credential_library_fkey + foreign key (project_id, store_id, public_id, credential_type) + references credential_library (project_id, store_id, public_id, credential_type) + on delete cascade + on update cascade, + constraint credential_vault_ssh_cert_valid_key_type_key_bits_fkey + foreign key (key_type, key_bits) + references credential_vault_ssh_cert_valid_key_type_key_bits(key_type, key_bits) + ); + comment on table credential_vault_ssh_cert_library is + 'credential_vault_ssh_cert_library a credential library that issues credentials from a vault ssh secret backend.'; + + create function default_ssh_certificate_credential_type() returns trigger + as $$ + begin + if new.credential_type is distinct from 'ssh_certificate' then + raise warning 'credential_vault_ssh_cert_library only supports ssh_certificate credentials'; + new.credential_type = 'ssh_certificate'; + end if; + return new; + end; + $$ language plpgsql; + comment on function default_ssh_certificate_credential_type is + 'default_ssh_certificate_credential_type ensures the credential_type is set to ssh_certificate'; + + create trigger default_ssh_certificate_credential_type before insert on credential_vault_ssh_cert_library + for each row execute procedure default_ssh_certificate_credential_type(); + create trigger insert_credential_library_subtype before insert on credential_vault_ssh_cert_library + for each row execute procedure insert_credential_library_subtype(); + create trigger default_create_time_column before insert on credential_vault_ssh_cert_library + for each row execute procedure default_create_time(); + create trigger delete_credential_library_subtype after delete on credential_vault_ssh_cert_library + for each row execute procedure delete_credential_library_subtype(); + create trigger immutable_columns before update on credential_vault_ssh_cert_library + for each row execute procedure immutable_columns('public_id', 'store_id', 'project_id', 'credential_type', 'create_time'); + create trigger update_time_column before update on credential_vault_ssh_cert_library + for each row execute procedure update_time_column(); + create trigger update_version_column after update on credential_vault_ssh_cert_library + for each row execute procedure update_version_column(); + create trigger before_insert_credential_vault_library before insert on credential_vault_ssh_cert_library + for each row execute procedure before_insert_credential_vault_library(); + + insert into oplog_ticket (name, version) + values + ('credential_vault_ssh_cert_library', 1); + +commit; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_ssh_cert_library.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_ssh_cert_library.sql new file mode 100644 index 000000000..f26ca74f0 --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_ssh_cert_library.sql @@ -0,0 +1,148 @@ +begin; + select plan(16); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate default values + prepare insert_valid as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc1', '/ssh/sign/foo', 'bar', 'ed25519', 0), + ('vs_______wvs', 'vl______vsc2', '/ssh/sign/foo', 'bar', 'ecdsa', 256), + ('vs_______wvs', 'vl______vsc3', '/ssh/sign/foo', 'bar', 'ecdsa', 384), + ('vs_______wvs', 'vl______vsc4', '/ssh/sign/foo', 'bar', 'ecdsa', 521), + ('vs_______wvs', 'vl______vsc5', '/ssh/sign/foo', 'bar', 'rsa', 2048), + ('vs_______wvs', 'vl______vsc6', '/ssh/sign/foo', 'bar', 'rsa', 3072), + ('vs_______wvs', 'vl______vsc7', '/ssh/sign/foo', 'bar', 'rsa', 4096); + + prepare select_vault_ssh_cert_libraries as + select public_id::text, store_id::text, name::text, description::text, vault_path, username, key_type, key_bits, ttl, key_id, critical_options, extensions, credential_type, project_id::text + from credential_vault_ssh_cert_library + where public_id like 'vl______vsc%' + order by public_id; + + prepare select_libraries as + select public_id::text, store_id::text, credential_type, project_id::text + from credential_library + where public_id in ('vl______vsc1', 'vl______vsc3') + order by public_id; + + select lives_ok('insert_valid'); + select results_eq( + 'select_vault_ssh_cert_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc2', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 256, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc3', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 384, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc4', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 521, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc5', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 2048, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc6', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 3072, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc7', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 4096, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget')$$ + ); + + + prepare insert_invalid_key_type as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'unknown', 256); + select throws_ok('insert_invalid_key_type', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_key_bits as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'rsa', 99); + select throws_ok('insert_invalid_key_bits', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_rsa_key_type_bits_combo as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'rsa', 384); + select throws_ok('insert_invalid_rsa_key_type_bits_combo', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_ecdsa_key_type_bits_combo as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'ecdsa', 2048); + select throws_ok('insert_invalid_ecdsa_key_type_bits_combo', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_ed25519_key_type_bits_combo as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'ed25519', 2048); + select throws_ok('insert_invalid_ed25519_key_type_bits_combo', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_vault_path as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/not-sign/foo', 'bar', 'ed25519', 0); + select throws_ok('insert_invalid_vault_path', 'new row for relation "credential_vault_ssh_cert_library" violates check constraint "vault_path_must_be_sign_or_issue"'); + + prepare insert_invalid_vault_path_empty as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '', 'bar', 'ed25519', 0); + select throws_ok('insert_invalid_vault_path', 'new row for relation "credential_vault_ssh_cert_library" violates check constraint "vault_path_must_be_sign_or_issue"'); + + prepare insert_invalid_username as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', '', 'ed25519', 0); + select throws_ok('insert_invalid_username', 'new row for relation "credential_vault_ssh_cert_library" violates check constraint "username_must_not_be_empty"'); + + prepare insert_invalid_credential_type as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, credential_type, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'username_password', 'ed25519', 0); + select lives_ok('insert_invalid_credential_type'); + select results_eq( + 'select_vault_ssh_cert_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc2', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 256, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc3', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 384, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc4', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 521, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc5', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 2048, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc6', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 3072, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc7', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 4096, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc8', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget')$$ + ); + + select results_eq( + 'select_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', 'ssh_certificate', 'p____bwidget'), + ('vl______vsc3', 'vs_______wvs', 'ssh_certificate', 'p____bwidget')$$ + ); + + prepare delete_ssh_cred_library as + delete from credential_vault_ssh_cert_library where public_id = 'vl______vsc3'; + + select lives_ok('delete_ssh_cred_library'); + select results_eq( + 'select_vault_ssh_cert_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc2', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 256, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc4', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 521, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc5', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 2048, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc6', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 3072, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc7', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 4096, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc8', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget')$$ + ); + + select results_eq( + 'select_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', 'ssh_certificate', 'p____bwidget')$$ + ); + +rollback; diff --git a/internal/proto/controller/storage/credential/vault/store/v1/vault.proto b/internal/proto/controller/storage/credential/vault/store/v1/vault.proto index 1022bf44b..965ffb53d 100644 --- a/internal/proto/controller/storage/credential/vault/store/v1/vault.proto +++ b/internal/proto/controller/storage/credential/vault/store/v1/vault.proto @@ -265,6 +265,107 @@ message CredentialLibrary { string credential_type = 11; } +message SSHCertificateCredentialLibrary { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 1; + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // name is optional. If set, it must be unique within project_id. + // @inject_tag: `gorm:"default:null"` + string name = 4 [(custom_options.v1.mask_mapping) = { + this: "Name" + that: "name" + }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 5 [(custom_options.v1.mask_mapping) = { + this: "Description" + that: "description" + }]; + + // store_id of the owning vault credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string store_id = 6; + + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + uint32 version = 7; + + // vault_path is the path in Vault to request credentials from. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string vault_path = 8 [(custom_options.v1.mask_mapping) = { + this: "VaultPath" + that: "attributes.path" + }]; + + // username is the username to use when making an SSH connection. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string username = 9 [(custom_options.v1.mask_mapping) = { + this: "Username" + that: "attributes.username" + }]; + + // key_type specifies the key type to use when generating an SSH private key. + // Values must be "rsa", "ed25519", or "ecdsa". + // @inject_tag: `gorm:"not_null"` + string key_type = 10 [(custom_options.v1.mask_mapping) = { + this: "KeyType" + that: "attributes.key_type" + }]; + + // key_bits specifies the number of bits to use to generate an SSH private key. + // Not used if key_type is ed25519. + // @inject_tag: `gorm:"not_null"` + uint32 key_bits = 11 [(custom_options.v1.mask_mapping) = { + this: "KeyBits" + that: "attributes.key_bits" + }]; + + // ttl specifies the requested time to live for the certificate. + // @inject_tag: `gorm:"default:null"` + string ttl = 12 [(custom_options.v1.mask_mapping) = { + this: "Ttl" + that: "attributes.ttl" + }]; + + // key_id specifies the key id that the created certificate should have. + // @inject_tag: `gorm:"default:null"` + string key_id = 13 [(custom_options.v1.mask_mapping) = { + this: "KeyId" + that: "attributes.key_id" + }]; + + // critical_options specifies a map of the critical options that the certificate should be signed for. + // @inject_tag: `gorm:"default:null"` + string critical_options = 14 [(custom_options.v1.mask_mapping) = { + this: "CriticalOptions" + that: "attributes.critical_options" + }]; + + // extensions specifies a map of the extensions that the certificate should be signed for. + // @inject_tag: `gorm:"default:null"` + string extensions = 15 [(custom_options.v1.mask_mapping) = { + this: "Extensions" + that: "attributes.extensions" + }]; + + // credential_type is always ssh_certificate + // @inject_tag: `gorm:"default:null"` + string credential_type = 16; +} + message Credential { // public_id is a surrogate key suitable for use in a public API. // @inject_tag: `gorm:"primary_key"` From 3a78a74571cf3f98cec023bcc9c6f12f512b4ccb Mon Sep 17 00:00:00 2001 From: Danielle <29378233+DanielleMiu@users.noreply.github.com> Date: Mon, 30 Jan 2023 22:17:53 +0000 Subject: [PATCH 03/11] feat(vault): Support Issuing SSH Cert Credentials Allows the new credential library type to be pulled from the db and used for issuing ssh certificate type credentials. The ssh cert library interfaces with vault's ssh secrets engine to either issu certificates directly from vault, or to generate them within boundary and have vault sign them as a certificate authority for authentication. See: https://developer.hashicorp.com/vault/api-docs/secret/ssh#sign-ssh-key https://developer.hashicorp.com/vault/api-docs/secret/ssh#generate-certificate-and-key --- go.mod | 1 + go.sum | 2 + internal/credential/credential.go | 7 + internal/credential/vault/private_library.go | 476 ++++++++++++++++-- .../credential/vault/private_library_test.go | 174 ++++++- internal/credential/vault/supported.go | 7 +- internal/credential/vault/testing.go | 141 +++++- ...add_data_key_foreign_key_references.up.sql | 1 + ...ssh_cert_to_vault_cred_library_view.up.sql | 111 ++++ 9 files changed, 865 insertions(+), 55 deletions(-) create mode 100644 internal/db/schema/migrations/oss/postgres/63/02_add_ssh_cert_to_vault_cred_library_view.up.sql diff --git a/go.mod b/go.mod index 6dd186b56..68ab294b9 100644 --- a/go.mod +++ b/go.mod @@ -161,6 +161,7 @@ require ( github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index 8966004b2..202734ace 100644 --- a/go.sum +++ b/go.sum @@ -980,6 +980,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw= diff --git a/internal/credential/credential.go b/internal/credential/credential.go index d09e4cc60..edee5fbf7 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -98,6 +98,13 @@ type Request struct { Purpose Purpose } +// SshCertificate is a credential containing a client certificate, username, +// and SSH private key. +type SshCertificate interface { + SshPrivateKey + Certificate() []byte +} + // Issuer issues dynamic credentials. type Issuer interface { // Issue issues dynamic credentials for a session from the requested diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 712b84765..a161f4ce1 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -2,8 +2,18 @@ package vault import ( "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" "database/sql" + "encoding/base64" + "encoding/json" + "encoding/pem" "fmt" + "regexp" "strings" "time" @@ -17,6 +27,8 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" vault "github.com/hashicorp/vault/api" + "github.com/mikesmitty/edkey" + "golang.org/x/crypto/ssh" "google.golang.org/protobuf/proto" ) @@ -25,13 +37,13 @@ var _ credential.Dynamic = (*baseCred)(nil) type baseCred struct { *Credential - lib *genericIssuingCredentialLibrary + lib issuingCredentialLibrary secretData map[string]any } func (bc *baseCred) Secret() credential.SecretData { return bc.secretData } func (bc *baseCred) Library() credential.Library { return bc.lib } -func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.Purpose } +func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.GetPurpose() } func (bc *baseCred) getExpiration() time.Duration { return bc.expiration } func (bc *baseCred) getCredential() *Credential { return bc.Credential } @@ -68,7 +80,12 @@ func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) { return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type")) } - uAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PasswordAttribute + lib, ok := bc.lib.(*genericIssuingCredentialLibrary) + if !ok { + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("baseCred.lib is not of type genericIssuingCredentialLibrary")) + } + + uAttr, pAttr := lib.UsernameAttribute, lib.PasswordAttribute if uAttr == "" { uAttr = "username" } @@ -110,7 +127,12 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type")) } - uAttr, pkAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PrivateKeyAttribute, bc.lib.PrivateKeyPassphraseAttribute + lib, ok := bc.lib.(*genericIssuingCredentialLibrary) + if !ok { + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("baseCred.lib is not of type genericIssuingCredentialLibrary")) + } + + uAttr, pkAttr, pAttr := lib.UsernameAttribute, lib.PrivateKeyAttribute, lib.PrivateKeyPassphraseAttribute if uAttr == "" { uAttr = "username" } @@ -133,10 +155,18 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err }, nil } +type sshCertCred struct { + *sshPrivateKeyCred + certificate []byte +} + +func (c *sshCertCred) Certificate() []byte { return c.certificate } + var _ credential.Library = (*genericIssuingCredentialLibrary)(nil) type issuingCredentialLibrary interface { - GetPublicId() string + credential.Library + GetPurpose() credential.Purpose retrieveCredential(context.Context, errors.Op, ...credential.Option) (dynamicCred, error) } @@ -221,6 +251,7 @@ func (pl *genericIssuingCredentialLibrary) GetDescription() string func (pl *genericIssuingCredentialLibrary) GetVersion() uint32 { return pl.Version } func (pl *genericIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } func (pl *genericIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } +func (pl *genericIssuingCredentialLibrary) GetPurpose() credential.Purpose { return pl.Purpose } func (pl *genericIssuingCredentialLibrary) CredentialType() credential.Type { switch ct := pl.CredType; ct { @@ -444,6 +475,14 @@ type privateCredentialLibraryAllTypes struct { PrivateKeyAttribute string PrivateKeyPassphraseAttribute string Purpose credential.Purpose `gorm:"-"` + KeyType string + KeyBits int + Username string + Ttl string + KeyId string + CriticalOptions []byte + Extensions []byte + CredLibType string } func (pl *privateCredentialLibraryAllTypes) GetPublicId() string { return pl.PublicId } @@ -516,42 +555,88 @@ func (pl *privateCredentialLibraryAllTypes) clone() *privateCredentialLibraryAll CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), ClientKeyId: pl.ClientKeyId, Purpose: pl.Purpose, + KeyType: pl.KeyType, + KeyBits: pl.KeyBits, + Username: pl.Username, + Ttl: pl.Ttl, + KeyId: pl.KeyId, + CriticalOptions: pl.CriticalOptions, + Extensions: pl.Extensions, + CredLibType: pl.CredLibType, } } func (pl *privateCredentialLibraryAllTypes) toTypedIssuingCredentialLibrary() issuingCredentialLibrary { - return &genericIssuingCredentialLibrary{ - PublicId: pl.PublicId, - StoreId: pl.StoreId, - CredType: pl.CredType, - UsernameAttribute: pl.UsernameAttribute, - PasswordAttribute: pl.PasswordAttribute, - PrivateKeyAttribute: pl.PrivateKeyAttribute, - PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute, - Name: pl.Name, - Description: pl.Description, - CreateTime: pl.CreateTime, - UpdateTime: pl.UpdateTime, - Version: pl.Version, - ProjectId: pl.ProjectId, - VaultPath: pl.VaultPath, - HttpMethod: pl.HttpMethod, - HttpRequestBody: pl.HttpRequestBody, - VaultAddress: pl.VaultAddress, - Namespace: pl.Namespace, - CaCert: pl.CaCert, - TlsServerName: pl.TlsServerName, - TlsSkipVerify: pl.TlsSkipVerify, - WorkerFilter: pl.WorkerFilter, - TokenHmac: pl.TokenHmac, - Token: pl.Token, - CtToken: pl.CtToken, - TokenKeyId: pl.TokenKeyId, - ClientCert: pl.ClientCert, - ClientKey: pl.ClientKey, - CtClientKey: pl.CtClientKey, - ClientKeyId: pl.ClientKeyId, - Purpose: pl.Purpose, + switch pl.CredLibType { + case "ssh-signed-cert": + return &sshCertIssuingCredentialLibrary{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + Username: pl.Username, + Name: pl.Name, + Description: pl.Description, + CreateTime: pl.CreateTime, + UpdateTime: pl.UpdateTime, + Version: pl.Version, + ProjectId: pl.ProjectId, + VaultPath: pl.VaultPath, + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: pl.CaCert, + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + WorkerFilter: pl.WorkerFilter, + TokenHmac: pl.TokenHmac, + Token: pl.Token, + CtToken: pl.CtToken, + TokenKeyId: pl.TokenKeyId, + ClientCert: pl.ClientCert, + ClientKey: pl.ClientKey, + CtClientKey: pl.CtClientKey, + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, + KeyType: pl.KeyType, + KeyBits: pl.KeyBits, + Ttl: pl.Ttl, + KeyId: pl.KeyId, + CriticalOptions: pl.CriticalOptions, + Extensions: pl.Extensions, + } + default: + return &genericIssuingCredentialLibrary{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + PrivateKeyAttribute: pl.PrivateKeyAttribute, + PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: pl.CreateTime, + UpdateTime: pl.UpdateTime, + Version: pl.Version, + ProjectId: pl.ProjectId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: pl.HttpRequestBody, + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: pl.CaCert, + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + WorkerFilter: pl.WorkerFilter, + TokenHmac: pl.TokenHmac, + Token: pl.Token, + CtToken: pl.CtToken, + TokenKeyId: pl.TokenKeyId, + ClientCert: pl.ClientCert, + ClientKey: pl.ClientKey, + CtClientKey: pl.CtClientKey, + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, + } } } @@ -597,3 +682,320 @@ func (m *requestMap) libIds() []string { func (m *requestMap) get(libraryId string) []credential.Purpose { return m.ids[libraryId] } + +type sshCertIssuingCredentialLibrary struct { + PublicId string + StoreId string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + VaultPath string + CredType string + ProjectId string + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + WorkerFilter string + Token TokenSecret + CtToken []byte + TokenHmac []byte + TokenKeyId string + ClientCert []byte + ClientKey KeySecret + CtClientKey []byte + ClientKeyId string + Username string + KeyType string + KeyBits int + KeyId string + Ttl string + CriticalOptions []byte + Extensions []byte + Purpose credential.Purpose +} + +func (lib *sshCertIssuingCredentialLibrary) GetPublicId() string { return lib.PublicId } +func (lib *sshCertIssuingCredentialLibrary) GetStoreId() string { return lib.StoreId } +func (lib *sshCertIssuingCredentialLibrary) GetName() string { return lib.Name } +func (lib *sshCertIssuingCredentialLibrary) GetDescription() string { return lib.Description } +func (lib *sshCertIssuingCredentialLibrary) GetVersion() uint32 { return lib.Version } +func (lib *sshCertIssuingCredentialLibrary) GetPurpose() credential.Purpose { return lib.Purpose } +func (lib *sshCertIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { + return lib.CreateTime +} + +func (lib *sshCertIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { + return lib.UpdateTime +} + +func (lib *sshCertIssuingCredentialLibrary) CredentialType() credential.Type { + switch ct := lib.CredType; ct { + case "": + return credential.UnspecifiedType + default: + return credential.Type(ct) + } +} + +func (lib *sshCertIssuingCredentialLibrary) client(ctx context.Context) (vaultClient, error) { + const op = "vault.(genericIssuingCredentialLibrary).client" + clientConfig := &clientConfig{ + Addr: lib.VaultAddress, + Token: lib.Token, + CaCert: lib.CaCert, + TlsServerName: lib.TlsServerName, + TlsSkipVerify: lib.TlsSkipVerify, + Namespace: lib.Namespace, + } + + if lib.ClientKey != nil { + clientConfig.ClientCert = lib.ClientCert + clientConfig.ClientKey = lib.ClientKey + } + + client, err := vaultClientFactoryFn(ctx, clientConfig, WithWorkerFilter(lib.WorkerFilter)) + if err != nil { + return nil, errors.WrapDeprecated(err, op, errors.WithMsg("unable to create vault client")) + } + return client, nil +} + +func generatePublicPrivateKeys(ctx context.Context, keyType string, keyBits int) (string, []byte, error) { + const op = "vault.generatePublicPrivateKeys" + pemBlock := pem.Block{} + var sshKey ssh.PublicKey + + switch keyType { + case KeyTypeRsa: + pemBlock.Type = "RSA PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go + + key, err := rsa.GenerateKey(rand.Reader, keyBits) + if err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + if sshKey, err = ssh.NewPublicKey(&key.PublicKey); err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + pemBlock.Bytes = x509.MarshalPKCS1PrivateKey(key) + + case KeyTypeEd25519: + pemBlock.Type = "OPENSSH PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go + + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + if sshKey, err = ssh.NewPublicKey(pubKey); err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + if pemBlock.Bytes = edkey.MarshalED25519PrivateKey(privKey); pemBlock.Bytes == nil { + return "", nil, errors.New(ctx, errors.Encode, op, "failed to marshal ed25519 private key") + } + + case KeyTypeEcdsa: + pemBlock.Type = "EC PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go + + var curve elliptic.Curve + switch keyBits { + case 256: + curve = elliptic.P256() + case 384: + curve = elliptic.P384() + case 521: + curve = elliptic.P521() + default: + return "", nil, errors.New(ctx, errors.InvalidParameter, op, "invalid KeyBits. when KeyType=ecdsa, KeyBits must be one of: 256, 384, or 521") + } + + key, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + if sshKey, err = ssh.NewPublicKey(&key.PublicKey); err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + if pemBlock.Bytes, err = x509.MarshalECPrivateKey(key); err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + default: + return "", nil, errors.New(ctx, errors.InvalidParameter, op, "invalid KeyType, must be one of: \"rsa\", \"ed25519\", or \"ecdsa\"") + } + + publicKey := base64.StdEncoding.EncodeToString(sshKey.Marshal()) + privateKey := pem.EncodeToMemory(&pemBlock) + if privateKey == nil { + return "", nil, errors.New(ctx, errors.Encode, op, "failed to encode private key to PEM format") + } + return publicKey, privateKey, nil +} + +type sshCertVaultBody struct { + KeyType string `json:"key_type,omitempty"` // must be "rsa", "ed25519", or "ecdsa" + KeyBits int `json:"key_bits,omitempty"` // with key_type=rsa, allowed values are: 2048 (default), 3072, or 4096; with key_type=ecdsa, allowed values are: 256 (default), 384, or 521; ignored with key_type=ed25519 + PublicKey string `json:"public_key,omitempty"` + TTL string `json:"ttl,omitempty"` + ValidPrincipals string `json:"valid_principals,omitempty"` // this needs to be "generated" off of the username provided in config + CertType string `json:"cert_type,omitempty"` // this should always be "user" + KeyId string `json:"key_id,omitempty"` // this will be loaded directly from lib + CriticalOptions map[string]string `json:"critical_options,omitempty"` // this will be loaded directly from lib + Extensions map[string]string `json:"extensions,omitempty"` // this will be loaded directly from lib +} + +var vaultPathRegexp = regexp.MustCompile(`^.+\/(sign|issue)\/[^\/\\\s]+$`) + +// retrieveCredential retrieves a dynamic connection credential from Vault +// for a specific session and connection. +// +// Supported options: credential.WithTemplateData +func (lib *sshCertIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, opt ...credential.Option) (dynamicCred, error) { + opts, err := credential.GetOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + // Get the credential ID early. No need to get a secret from Vault + // if there is no way to save it in the database. + credId, err := newCredentialId() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + client, err := lib.client(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + // build as much of the payload as we can, since sign/issue share many attributes + // Template the username + tplate, err := template.New(ctx, lib.Username) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + username, err := tplate.Generate(ctx, opts.WithTemplateData) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var criticalOptions map[string]string + if lib.CriticalOptions != nil { + if json.Unmarshal(lib.CriticalOptions, &criticalOptions) != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + + var extensions map[string]string + if lib.Extensions != nil { + if json.Unmarshal(lib.Extensions, &extensions) != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + + payload := sshCertVaultBody{ + ValidPrincipals: username, + CertType: "user", + CriticalOptions: criticalOptions, + Extensions: extensions, + TTL: lib.Ttl, + KeyId: lib.KeyId, + } + + var privateKey credential.PrivateKey + var secret *vault.Secret + + match := vaultPathRegexp.FindStringSubmatch(lib.VaultPath) + if len(match) < 2 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "vault path was not in an expected format. expected path containing \"sign\" or \"issue\"") + } + + // by definition, if match exists, then match[1] == "sign" or "issue" + switch match[1] { + case "sign": + payload.PublicKey, privateKey, err = generatePublicPrivateKeys(ctx, lib.KeyType, lib.KeyBits) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + secret, err = client.post(ctx, lib.VaultPath, body) + if err != nil { + // TODO(mgaffney) 05/2021: detect if the error is because of an + // expired or invalid token + return nil, errors.Wrap(ctx, err, op) + } + if secret == nil { + return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op)) + } + + case "issue": + payload.KeyBits = lib.KeyBits + if lib.KeyType == KeyTypeEcdsa { + // this is a special case where internal to boundary, we refer to the crypto + // library name, but vault refers to it simply as "ec" for "Elliptic Curve" + payload.KeyType = "ec" + } else { + payload.KeyType = lib.KeyType + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + secret, err = client.post(ctx, lib.VaultPath, body) + if err != nil { + // TODO(mgaffney) 05/2021: detect if the error is because of an + // expired or invalid token + return nil, errors.Wrap(ctx, err, op) + } + if secret == nil { + return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op)) + } + + pk, ok := secret.Data["private_key"].(string) + if !ok { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a private key or response was not in the expected format") + } + + privateKey = []byte(pk) + } + + leaseDuration := time.Duration(secret.LeaseDuration) * time.Second + cred, err := newCredential(lib.GetPublicId(), secret.LeaseID, lib.TokenHmac, leaseDuration) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cred.PublicId = credId + cred.IsRenewable = secret.Renewable // possibly set to just false + + // same location for both + cert, ok := secret.Data["signed_key"].(string) + if !ok { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a signed key or response was not in the expected format") + } + + return &sshCertCred{ + sshPrivateKeyCred: &sshPrivateKeyCred{ + baseCred: &baseCred{ + Credential: cred, + lib: lib, + secretData: secret.Data, + }, + username: username, + privateKey: privateKey, + }, + certificate: []byte(cert), + }, nil +} diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index dca8b671c..ed0fff6bf 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -2,7 +2,9 @@ package vault import ( "context" + "fmt" "testing" + "time" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db" @@ -10,6 +12,8 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/scheduler" + "github.com/hashicorp/boundary/internal/util" + "github.com/hashicorp/boundary/internal/util/template" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -63,7 +67,7 @@ func TestRepository_getPrivateLibraries(t *testing.T) { opts = append(opts, WithClientCert(clientCert)) } - _, token := v.CreateToken(t) + _, token := v.CreateToken(t, WithTokenPeriod(time.Hour)) credStoreIn, err := NewCredentialStore(prj.GetPublicId(), v.Addr, []byte(token), opts...) assert.NoError(err) @@ -1215,3 +1219,171 @@ func TestBaseToSshPriKey(t *testing.T) { }) } } + +func TestRepository_sshCertIssuingCredentialLibrary_retrieveCredential(t *testing.T) { + t.Parallel() + + // create test vault server + v := NewTestVaultServer(t, WithTestVaultTLS(TestNoTLS), WithVaultVersion("1.12.2")) + require.NotNil(t, v) + + vc := v.client(t).cl + mounts, err := vc.Sys().ListMounts() + assert.NoError(t, err) + require.NotEmpty(t, mounts) + beforeCount := len(mounts) + + // enable ssh secrets engine + v.MountSSH(t, WithAllowedExtension("permit-pty")) + mounts, err = vc.Sys().ListMounts() + fmt.Printf("mounts: %#v\n", mounts) + assert.NoError(t, err) + require.NotEmpty(t, mounts) + afterCount := len(mounts) + assert.Greater(t, afterCount, beforeCount) + + // create and setup db + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + sec, token := v.CreateToken(t, WithPolicies([]string{"default", "boundary-controller", "ssh"}), WithTokenPeriod(time.Hour)) + + sche := scheduler.TestScheduler(t, conn, wrapper) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + require.NoError(t, err) + require.NotNil(t, repo) + + cs := TestCredentialStore(t, conn, wrapper, prj.GetPublicId(), v.Addr, token, sec.Auth.Accessor) + + tests := []struct { + name string + username string + vaulthPath string + opts []Option + retOpts []credential.Option + expected map[string]string + }{ + { + name: "vault sign boundary ed25519 key", + username: "username", + vaulthPath: "ssh/sign/boundary", + opts: []Option{WithKeyType(KeyTypeEd25519)}, + }, + { + name: "vault sign boundary rsa(4096) key", + username: "username-2-electric-boogaloo", + vaulthPath: "ssh/sign/boundary", + opts: []Option{WithKeyType(KeyTypeRsa), WithKeyBits(4096)}, + }, + { + name: "vault sign boundary ec(521) key", + username: "username-3-the-namening", + vaulthPath: "ssh/sign/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(521)}, + }, + { + name: "vault issue ed25519 cert", + username: "username-4-revengeance", + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEd25519)}, + }, + { + name: "vault issue rsa(4096) cert", + username: "username-5-this-time-its-personal", + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeRsa), WithKeyBits(4096)}, + }, + { + name: "vault issue ec(521) cert", + username: "username-6-the-holiday-episode", + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(521)}, + }, + { + name: "vault issue rsa(2048) cert with critical options and extensions", + username: "username-7-because-789", + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeRsa), WithKeyBits(2048), WithCriticalOptions("{ \"force-commnad\": \"/bin/some-script\" }"), WithExtensions("{ \"permit-pty\": \"\" }")}, + }, + { + name: "vault sign boundary rsa(3072) key with critical options and extensions", + username: "username-7-because-789", + vaulthPath: "ssh/sign/boundary", + opts: []Option{WithKeyType(KeyTypeRsa), WithKeyBits(3072), WithCriticalOptions("{ \"force-commnad\": \"/bin/some-script\" }"), WithExtensions("{ \"permit-pty\": \"\" }")}, + }, + { + name: "vault issue ec(256) cert with template username", + username: "username-8-{{ .User.Name }}", + expected: map[string]string{ + "username": "username-8-revenge-of-the-template", + }, + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(256)}, + retOpts: []credential.Option{credential.WithTemplateData(template.Data{User: template.User{Name: util.Pointer("revenge-of-the-template")}})}, + }, + { + name: "vault issue ec(384) cert with disallowed extension", + username: "username-10-because-789", + expected: map[string]string{ + "error": "extensions [permit-port-forwarding] are not on allowed list", + }, + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(384), WithExtensions("{ \"permit-port-forwarding\": \"\" }")}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + // create ssh library + lib, err := NewSSHCertificateCredentialLibrary(cs.GetPublicId(), tt.vaulthPath, tt.username, tt.opts...) + require.NoError(err) + require.NotNil(lib) + lib.PublicId, err = newSSHCertificateCredentialLibraryId() + require.NoError(err) + + _, err = rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + return iw.Create(ctx, lib) + }, + ) + require.NoError(err) + + req := credential.Request{ + SourceId: lib.GetPublicId(), + Purpose: "doesn't matter", + } + + libs, err := repo.getIssueCredLibraries(ctx, []credential.Request{req}) + require.NoError(err) + require.NotEmpty(libs) + require.Equal(1, len(libs)) + + cred, err := libs[0].retrieveCredential(ctx, "op", tt.retOpts...) + if retErr, ok := tt.expected["error"]; ok { + require.ErrorContains(err, retErr) + return // retrieveCredential failed (as expected) don't do the rest of the checks + } + require.NoError(err) + + cert, ok := cred.(*sshCertCred) + require.True(ok) + + if usr, ok := tt.expected["username"]; ok { + require.Equal(usr, cert.username) + } + + // TODO: somehow check that the ssh cert matches the private key + // for now, manually check that private key and cert match and that sign/issue both return correct formats + fmt.Printf("test: %s\npriv:\n%s\ncert:\n%s\n", tt.name, string(cert.privateKey), string(cert.certificate)) + + // ensure credential matches expected SshCertificate + _, ok = cred.(credential.SshCertificate) + require.True(ok) + }) + } +} diff --git a/internal/credential/vault/supported.go b/internal/credential/vault/supported.go index 90b040217..37a8563a0 100644 --- a/internal/credential/vault/supported.go +++ b/internal/credential/vault/supported.go @@ -75,9 +75,14 @@ func gotNewServer(t testing.TB, opt ...TestOption) *TestVaultServer { pool: pool, } + vaultVersion := DefaultVaultVersion + if opts.vaultVersion != "" { + vaultVersion = opts.vaultVersion + } + dockerOptions := &dockertest.RunOptions{ Repository: "vault", - Tag: DefaultVaultVersion, + Tag: vaultVersion, Env: []string{fmt.Sprintf("VAULT_DEV_ROOT_TOKEN_ID=%s", server.RootToken)}, } diff --git a/internal/credential/vault/testing.go b/internal/credential/vault/testing.go index d87171f84..acdac4c60 100644 --- a/internal/credential/vault/testing.go +++ b/internal/credential/vault/testing.go @@ -140,10 +140,10 @@ func TestCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, stor return libs } -// TestSSHCertificateCredentialLibraries creates count number of vault credential -// libraries in the provided DB with the provided store id. If any errors -// are encountered during the creation of the credential libraries, the -// test will fail. +// TestSSHCertificateCredentialLibraries creates count number of vault ssh +// certificate credential libraries in the provided DB with the provided store +// id. If any errors are encountered during the creation of the credential +// libraries, the test will fail. func TestSSHCertificateCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, storeId string, count int) []*SSHCertificateCredentialLibrary { t.Helper() assert, require := assert.New(t), require.New(t) @@ -151,7 +151,7 @@ func TestSSHCertificateCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping var libs []*SSHCertificateCredentialLibrary for i := 0; i < count; i++ { - lib, err := NewSSHCertificateCredentialLibrary(storeId, fmt.Sprintf("vault/path%d", i), "username", withKeyType(KeyTypeEd25519)) + lib, err := NewSSHCertificateCredentialLibrary(storeId, fmt.Sprintf("ssh/sign/role-%d", i), "username", WithKeyType(KeyTypeEd25519)) assert.NoError(err) require.NotNil(lib) id, err := newSSHCertificateCredentialLibraryId() @@ -509,17 +509,19 @@ type TestOption func(testing.TB, *testOptions) // options = how options are represented type testOptions struct { - orphan bool - periodic bool - renewable bool - policies []string - mountPath string - roleName string - vaultTLS TestVaultTLS - dockerNetwork bool - skipCleanup bool - tokenPeriod time.Duration - clientKey *ecdsa.PrivateKey + orphan bool + periodic bool + renewable bool + policies []string + allowedExtensions []string + mountPath string + roleName string + vaultTLS TestVaultTLS + dockerNetwork bool + skipCleanup bool + tokenPeriod time.Duration + clientKey *ecdsa.PrivateKey + vaultVersion string } func getDefaultTestOptions(t testing.TB) testOptions { @@ -625,6 +627,24 @@ func WithDontCleanUp() TestOption { } } +// WithVaultVersion sets the version of vault that will be started. +// defaults to the value stored in vault.supported.DefaultVaultVersion +func WithVaultVersion(s string) TestOption { + return func(t testing.TB, o *testOptions) { + t.Helper() + o.vaultVersion = s + } +} + +// WithAllowedExtension tells vault to allow a specific SSH extension +// to be used by vault's ssh secrets engine +func WithAllowedExtension(e string) TestOption { + return func(t testing.TB, o *testOptions) { + t.Helper() + o.allowedExtensions = append(o.allowedExtensions, e) + } +} + // TestOrphanToken sets the token orphan option to b. // The orphan option is true by default. func TestOrphanToken(b bool) TestOption { @@ -784,6 +804,95 @@ func (v *TestVaultServer) addPolicy(t testing.TB, name string, pc pathCapabiliti require.NoError(vc.Sys().PutPolicy(name, policy)) } +// MountSSH mounts the Vault SSH secret engine and initializes it by +// generating a root certificate authority and creating a default role on +// the mount. The root CA is returned. +// +// The default mount path is ssh and the default role name is boundary. +// WithTestMountPath, WithTestRoleName, and WithAllowedExtension are the +// test options supported. extensions are combined into allowed_extensions. +// allowed_extensions defaults to "*" (allow all) +// The role is defined as: +// +// { +// "key_type": "ca", +// "allowed_users": "*", +// "allowed_extensions": "*", +// "allow_user_certificates": true, +// "ttl": "12h0m0s" +// } +// +// MountSSH also adds a Vault policy named 'ssh' to v and adds it to the +// standard set of polices attached to tokens created with v.CreateToken. +// The policy is defined as: +// +// path "mountPath/*" { +// capabilities = ["create", "read", "update", "delete", "list"] +// } +func (v *TestVaultServer) MountSSH(t testing.TB, opt ...TestOption) *vault.Secret { + require := require.New(t) + opts := getTestOpts(t, opt...) + vc := v.client(t).cl + + // Mount SSH + maxTTL := 24 * time.Hour + if t, ok := t.(*testing.T); ok { + if deadline, ok := t.Deadline(); ok { + maxTTL = time.Until(deadline) * 2 + } + } + + defaultTTL := maxTTL / 2 + t.Logf("maxTTL: %s, defaultTTL: %s", maxTTL, defaultTTL) + mountInput := &vault.MountInput{ + Type: "ssh", + Description: t.Name(), + Config: vault.MountConfigInput{ + DefaultLeaseTTL: defaultTTL.String(), + MaxLeaseTTL: maxTTL.String(), + }, + } + mountPath := opts.mountPath + if mountPath == "" { + mountPath = "ssh/" + } + // this is the call that actually enables the secrets engine + require.NoError(vc.Sys().Mount(mountPath, mountInput)) + + policyPath := fmt.Sprintf("%s*", mountPath) + pc := pathCapabilities{ + policyPath: createCapability | readCapability | updateCapability | deleteCapability | listCapability, + } + v.addPolicy(t, "ssh", pc) + + // Generate a root CA + caPath := path.Join(mountPath, "config/ca") + caOptions := map[string]any{ + "generate_signing_key": true, + } + s, err := vc.Logical().Write(caPath, caOptions) + require.NoError(err) + require.NotEmpty(s) + + // Create default role + rolePath := path.Join(mountPath, "roles", opts.roleName) + allowExtensions := "*" + if len(opts.allowedExtensions) > 0 { + allowExtensions = strings.Join(opts.allowedExtensions, ",") + } + roleOptions := map[string]any{ + "key_type": "ca", + "allowed_users": "*", + "allowed_extensions": allowExtensions, + "allow_user_certificates": true, + "ttl": defaultTTL.String(), + } + _, err = vc.Logical().Write(rolePath, roleOptions) + require.NoError(err) + + return s +} + // MountPKI mounts the Vault PKI secret engine and initializes it by // generating a root certificate authority and creating a default role on // the mount. The root CA is returned. diff --git a/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql b/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql index 430918693..3ed79b07e 100644 --- a/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql +++ b/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql @@ -188,6 +188,7 @@ begin; '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.'; + -- Replaced in 63/02_add_ssh_cert_to_vault_cred_library_view.up.sql create view credential_vault_library_issue_credentials as with password_override (library_id, username_attribute, password_attribute) as ( diff --git a/internal/db/schema/migrations/oss/postgres/63/02_add_ssh_cert_to_vault_cred_library_view.up.sql b/internal/db/schema/migrations/oss/postgres/63/02_add_ssh_cert_to_vault_cred_library_view.up.sql new file mode 100644 index 000000000..4b09d965b --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/63/02_add_ssh_cert_to_vault_cred_library_view.up.sql @@ -0,0 +1,111 @@ +begin; + + -- Replaces view from 56/02_add_data_key_foreign_key_references.up.sql + drop view credential_vault_library_issue_credentials; + 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, + null as key_type, + null as key_bits, + null as username, + null as ttl, + null as key_id, + null as critical_options, + null as extensions, + 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, + 'generic' as cred_lib_type -- used to switch on + 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 + union + 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, + null as http_method, + null as http_request_body, + library.credential_type as credential_type, + library.key_type as key_type, + library.key_bits as key_bits, + library.username as username, + library.ttl as ttl, + library.key_id as key_id, + library.critical_options as critical_options, + library.extensions as extensions, + 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, + null as username_attribute, + null as password_attribute, + null as private_key_attribute, + null as private_key_passphrase_attribute, + 'ssh-signed-cert' as cred_lib_type -- used to switch on + from credential_vault_ssh_cert_library library + join credential_vault_store_client store + on library.store_id = store.public_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.'; + +commit; From 43f0ba89cf5ee1efd415ba58b13cf1a03794455c Mon Sep 17 00:00:00 2001 From: Haotian Date: Mon, 30 Jan 2023 22:14:58 +0000 Subject: [PATCH 04/11] feat(credentiallibraries): support vault ssh certificates --- .../credential_library.gen.go | 7 +- api/credentiallibraries/option.gen.go | 168 ++++ ...icate_credential_library_attributes.gen.go | 44 + internal/api/genapi/input.go | 48 +- .../vault_credentiallibraries.gen.go | 2 +- internal/cmd/gencli/input.go | 25 +- .../credentiallibrary_service.go | 489 ++++++++-- .../credentiallibrary_service_test.go | 912 +++++++++++++++++- .../targets/tcp/target_service_test.go | 27 +- .../v1/credential_library.proto | 90 +- .../credentiallibrary_test.go | 11 +- internal/tests/api/targets/target_test.go | 8 +- .../credential_library.pb.go | 491 +++++++--- .../credential_store_test.go | 4 +- 14 files changed, 2087 insertions(+), 239 deletions(-) create mode 100644 api/credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go diff --git a/api/credentiallibraries/credential_library.gen.go b/api/credentiallibraries/credential_library.gen.go index 4eb7d8cee..ba315aa1c 100644 --- a/api/credentiallibraries/credential_library.gen.go +++ b/api/credentiallibraries/credential_library.gen.go @@ -90,7 +90,7 @@ func (c *Client) ApiClient() *api.Client { return c.client } -func (c *Client) Create(ctx context.Context, credentialStoreId string, opt ...Option) (*CredentialLibraryCreateResult, error) { +func (c *Client) Create(ctx context.Context, resourceType string, credentialStoreId string, opt ...Option) (*CredentialLibraryCreateResult, error) { if credentialStoreId == "" { return nil, fmt.Errorf("empty credentialStoreId value passed into Create request") } @@ -100,6 +100,11 @@ func (c *Client) Create(ctx context.Context, credentialStoreId string, opt ...Op if c.client == nil { return nil, fmt.Errorf("nil client") } + if resourceType == "" { + return nil, fmt.Errorf("empty resourceType value passed into Create request") + } else { + opts.postMap["type"] = resourceType + } opts.postMap["credential_store_id"] = credentialStoreId diff --git a/api/credentiallibraries/option.gen.go b/api/credentiallibraries/option.gen.go index c63590351..12ba34836 100644 --- a/api/credentiallibraries/option.gen.go +++ b/api/credentiallibraries/option.gen.go @@ -110,6 +110,30 @@ func DefaultCredentialType() Option { } } +func WithVaultSSHCertificateCredentialLibraryCriticalOptions(inCriticalOptions map[string]string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["critical_options"] = inCriticalOptions + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryCriticalOptions() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["critical_options"] = nil + o.postMap["attributes"] = val + } +} + func WithDescription(inDescription string) Option { return func(o *options) { o.postMap["description"] = inDescription @@ -122,6 +146,30 @@ func DefaultDescription() Option { } } +func WithVaultSSHCertificateCredentialLibraryExtensions(inExtensions map[string]string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["extensions"] = inExtensions + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryExtensions() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["extensions"] = nil + o.postMap["attributes"] = val + } +} + func WithVaultCredentialLibraryHttpMethod(inHttpMethod string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -170,6 +218,78 @@ func DefaultVaultCredentialLibraryHttpRequestBody() Option { } } +func WithVaultSSHCertificateCredentialLibraryKeyBits(inKeyBits uint32) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_bits"] = inKeyBits + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryKeyBits() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_bits"] = nil + o.postMap["attributes"] = val + } +} + +func WithVaultSSHCertificateCredentialLibraryKeyId(inKeyId string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_id"] = inKeyId + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryKeyId() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_id"] = nil + o.postMap["attributes"] = val + } +} + +func WithVaultSSHCertificateCredentialLibraryKeyType(inKeyType string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_type"] = inKeyType + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryKeyType() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_type"] = nil + o.postMap["attributes"] = val + } +} + func WithName(inName string) Option { return func(o *options) { o.postMap["name"] = inName @@ -193,3 +313,51 @@ func WithVaultCredentialLibraryPath(inPath string) Option { o.postMap["attributes"] = val } } + +func WithVaultSSHCertificateCredentialLibraryPath(inPath string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["path"] = inPath + o.postMap["attributes"] = val + } +} + +func WithVaultSSHCertificateCredentialLibraryTtl(inTtl string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["ttl"] = inTtl + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryTtl() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["ttl"] = nil + o.postMap["attributes"] = val + } +} + +func WithVaultSSHCertificateCredentialLibraryUsername(inUsername string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["username"] = inUsername + o.postMap["attributes"] = val + } +} diff --git a/api/credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go b/api/credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go new file mode 100644 index 000000000..534e6f2f2 --- /dev/null +++ b/api/credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go @@ -0,0 +1,44 @@ +// Code generated by "make api"; DO NOT EDIT. +package credentiallibraries + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type VaultSSHCertificateCredentialLibraryAttributes struct { + Path string `json:"path,omitempty"` + Username string `json:"username,omitempty"` + KeyType string `json:"key_type,omitempty"` + KeyBits uint32 `json:"key_bits,omitempty"` + Ttl string `json:"ttl,omitempty"` + KeyId string `json:"key_id,omitempty"` + CriticalOptions map[string]string `json:"critical_options,omitempty"` + Extensions map[string]string `json:"extensions,omitempty"` +} + +func AttributesMapToVaultSSHCertificateCredentialLibraryAttributes(in map[string]interface{}) (*VaultSSHCertificateCredentialLibraryAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out VaultSSHCertificateCredentialLibraryAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *CredentialLibrary) GetVaultSSHCertificateCredentialLibraryAttributes() (*VaultSSHCertificateCredentialLibraryAttributes, error) { + if pt.Type != "vaultsshcertificate" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but credential-library is of type %s", "vaultsshcertificate", pt.Type) + } + return AttributesMapToVaultSSHCertificateCredentialLibraryAttributes(pt.Attributes) +} diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index ad660b226..807cebb10 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -518,12 +518,58 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, + { + inProto: &credentiallibraries.VaultSSHCertificateCredentialLibraryAttributes{}, + outFile: "credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go", + subtypeName: "VaultSSHCertificateCredentialLibrary", + fieldOverrides: []fieldInfo{ + { + Name: "Path", + SkipDefault: true, + }, + { + Name: "Username", + SkipDefault: true, + }, + { + Name: "CriticalOptions", + FieldType: "map[string]string", + }, + { + Name: "Extensions", + FieldType: "map[string]string", + }, + }, + parentTypeName: "CredentialLibrary", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &credentiallibraries.CredentialLibrary{}, outFile: "credentiallibraries/credential_library.gen.go", templates: []*template.Template{ clientTemplate, - commonCreateTemplate, + template.Must(template.New("").Funcs( + template.FuncMap{ + "snakeCase": snakeCase, + "funcName": func() string { + return "Create" + }, + "apiAction": func() string { + return "" + }, + "extraRequiredParams": func() []requiredParam { + return []requiredParam{ + { + Name: "resourceType", + Typ: "string", + PostType: "type", + }, + } + }, + }, + ).Parse(createTemplateStr)), readTemplate, updateTemplate, deleteTemplate, diff --git a/internal/cmd/commands/credentiallibrariescmd/vault_credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/vault_credentiallibraries.gen.go index 84a176816..7fdb932eb 100644 --- a/internal/cmd/commands/credentiallibrariescmd/vault_credentiallibraries.gen.go +++ b/internal/cmd/commands/credentiallibrariescmd/vault_credentiallibraries.gen.go @@ -203,7 +203,7 @@ func (c *VaultCommand) Run(args []string) int { switch c.Func { case "create": - createResult, err = credentiallibrariesClient.Create(c.Context, c.FlagCredentialStoreId, opts...) + createResult, err = credentiallibrariesClient.Create(c.Context, "vault", c.FlagCredentialStoreId, opts...) if exitCode := c.checkFuncError(err); exitCode > 0 { return exitCode } diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 00adaddef..20b6b2b61 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -227,18 +227,19 @@ var inputStructs = map[string][]*cmdInfo{ HasId: true, }, { - ResourceType: resource.CredentialLibrary.String(), - Pkg: "credentiallibraries", - StdActions: []string{"create", "update"}, - SubActionPrefix: "vault", - HasExtraCommandVars: true, - SkipNormalHelp: true, - HasExtraHelpFunc: true, - HasId: true, - HasName: true, - HasDescription: true, - Container: "CredentialStore", - VersionedActions: []string{"update"}, + ResourceType: resource.CredentialLibrary.String(), + Pkg: "credentiallibraries", + StdActions: []string{"create", "update"}, + SubActionPrefix: "vault", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + NeedsSubtypeInCreate: true, PrefixAttributeFieldErrorsWithSubactionPrefix: true, }, }, diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go index c8c3d03e1..d85d6f515 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -2,6 +2,7 @@ package credentiallibraries import ( "context" + "encoding/json" "fmt" "strings" @@ -33,6 +34,11 @@ const ( httpMethodField = "attributes.http_method" httpRequestBodyField = "attributes.http_request_body" credentialMappingPathField = "credential_mapping_overrides" + sshCertUsernameField = "attributes.username" + keyTypeField = "attributes.key_type" + keyBitsField = "attributes.key_bits" + criticalOptionsField = "attributes.critical_options" + extensionsField = "attributes.extensions" domain = "credential" ) @@ -45,7 +51,8 @@ const ( ) var ( - maskManager handlers.MaskManager + maskManager handlers.MaskManager + sshCertMaskManager handlers.MaskManager // IdActions contains the set of actions that can be performed on // individual resources @@ -62,6 +69,18 @@ var ( action.Create, action.List, } + + validCredentialTypesVaultGeneric = []credential.Type{ + credential.UsernamePasswordType, + credential.SshPrivateKeyType, + credential.UnspecifiedType, + } + + validKeyTypes = []string{ + vault.KeyTypeEcdsa, + vault.KeyTypeEd25519, + vault.KeyTypeRsa, + } ) func init() { @@ -70,6 +89,10 @@ func init() { handlers.MaskSource{&pb.CredentialLibrary{}, &pb.VaultCredentialLibraryAttributes{}}); err != nil { panic(err) } + if sshCertMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&store.SSHCertificateCredentialLibrary{}}, + handlers.MaskSource{&pb.CredentialLibrary{}, &pb.VaultSSHCertificateCredentialLibraryAttributes{}}); err != nil { + panic(err) + } } // Service handles request as described by the pbs.CredentialLibraryServiceServer interface. @@ -242,11 +265,23 @@ func (s Service) UpdateCredentialLibrary(ctx context.Context, req *pbs.UpdateCre if err != nil { return nil, err } - cur, err := repo.LookupCredentialLibrary(ctx, req.Id) - if err != nil { - return nil, err + var currentCredentialType credential.Type + var mo vault.MappingOverride + switch subtypes.SubtypeFromId(domain, req.GetId()) { + case vault.SSHCertificateLibrarySubtype: + cur, err := repo.LookupSSHCertificateCredentialLibrary(ctx, req.Id) + if err != nil { + return nil, err + } + currentCredentialType = credential.Type(cur.GetCredentialType()) + default: + cur, err := repo.LookupCredentialLibrary(ctx, req.Id) + if err != nil { + return nil, err + } + currentCredentialType = credential.Type(cur.GetCredentialType()) + mo = cur.MappingOverride } - currentCredentialType := credential.Type(cur.GetCredentialType()) if err := validateUpdateRequest(req, currentCredentialType); err != nil { return nil, err @@ -255,7 +290,7 @@ func (s Service) UpdateCredentialLibrary(ctx context.Context, req *pbs.UpdateCre if authResults.Error != nil { return nil, authResults.Error } - cl, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem(), currentCredentialType, cur.MappingOverride) + cl, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem(), currentCredentialType, mo) if err != nil { return nil, err } @@ -298,16 +333,28 @@ func (s Service) DeleteCredentialLibrary(ctx context.Context, req *pbs.DeleteCre return nil, nil } -func (s Service) listFromRepo(ctx context.Context, storeId string) ([]*vault.CredentialLibrary, error) { +func (s Service) listFromRepo(ctx context.Context, storeId string) ([]credential.Library, error) { const op = "credentiallibraries.(Service).listFromRepo" repo, err := s.repoFn() if err != nil { return nil, errors.Wrap(ctx, err, op) } - csl, err := repo.ListCredentialLibraries(ctx, storeId) + genCsl, err := repo.ListCredentialLibraries(ctx, storeId) if err != nil { return nil, errors.Wrap(ctx, err, op) } + certCsl, err := repo.ListSSHCertificateCredentialLibraries(ctx, storeId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + csl := make([]credential.Library, 0, len(genCsl)+len(certCsl)) + for _, s := range genCsl { + csl = append(csl, s) + } + for _, s := range certCsl { + csl = append(csl, s) + } return csl, nil } @@ -317,32 +364,67 @@ func (s Service) getFromRepo(ctx context.Context, id string) (credential.Library if err != nil { return nil, errors.Wrap(ctx, err, op) } - cs, err := repo.LookupCredentialLibrary(ctx, id) - if err != nil && !errors.IsNotFoundError(err) { - return nil, errors.Wrap(ctx, err, op) + switch subtypes.SubtypeFromId(domain, id) { + case vault.GenericLibrarySubtype: + cs, err := repo.LookupCredentialLibrary(ctx, id) + if err != nil && !errors.IsNotFoundError(err) { + return nil, errors.Wrap(ctx, err, op) + } + if cs == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("credential library %q not found", id)) + } + return cs, err + case vault.SSHCertificateLibrarySubtype: + cs, err := repo.LookupSSHCertificateCredentialLibrary(ctx, id) + if err != nil && !errors.IsNotFoundError(err) { + return nil, errors.Wrap(ctx, err, op) + } + if cs == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("ssh certificate credential library %q not found", id)) + } + return cs, err } - if cs == nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("credential library %q not found", id)) - } - return cs, err + return nil, errors.New(ctx, errors.InvalidParameter, op, "unrecognized credential library subtype") } func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.CredentialLibrary) (credential.Library, error) { const op = "credentiallibraries.(Service).createInRepo" - cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - repo, err := s.repoFn() - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - out, err := repo.CreateCredentialLibrary(ctx, scopeId, cl) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create credential library")) - } - if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential library but no error returned from repository.") + var out credential.Library + switch subtypes.SubtypeFromType(domain, item.GetType()) { + case vault.SSHCertificateLibrarySubtype: + cl, err := toStorageVaultSSHCertificateLibrary(item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + repo, err := s.repoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rl, err := repo.CreateSSHCertificateCredentialLibrary(ctx, scopeId, cl) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create ssh certificate credential library")) + } + if rl == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create ssh certificate credential library but no error returned from repository.") + } + out = rl + default: + cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + repo, err := s.repoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rl, err := repo.CreateCredentialLibrary(ctx, scopeId, cl) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create credential library")) + } + if rl == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential library but no error returned from repository.") + } + out = rl } return out, nil } @@ -372,26 +454,53 @@ func (s Service) updateInRepo( item.CredentialMappingOverrides = mappingStruct } - cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - cl.PublicId = id - - dbMasks = append(dbMasks, maskManager.Translate(masks)...) - if len(dbMasks) == 0 { - return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) - } + var out credential.Library + rowsUpdated := 0 repo, err := s.repoFn() if err != nil { return nil, errors.Wrap(ctx, err, op) } - out, rowsUpdated, err := repo.UpdateCredentialLibrary(ctx, projId, cl, item.GetVersion(), dbMasks) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) - } - if rowsUpdated == 0 { - return nil, handlers.NotFoundErrorf("Credential Library %q doesn't exist or incorrect version provided.", id) + switch subtypes.SubtypeFromId(domain, id) { + case vault.SSHCertificateLibrarySubtype: + dbMasks = append(dbMasks, sshCertMaskManager.Translate(masks)...) + if getMapUpdate(criticalOptionsField, masks) { + dbMasks = append(dbMasks, vault.CriticalOptionsField) + } + if getMapUpdate(extensionsField, masks) { + dbMasks = append(dbMasks, vault.ExtensionsField) + } + if len(dbMasks) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + cl, err := toStorageVaultSSHCertificateLibrary(item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cl.PublicId = id + out, rowsUpdated, err = repo.UpdateSSHCertificateCredentialLibrary(ctx, projId, cl, item.GetVersion(), dbMasks) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Credential Library %q doesn't exist or incorrect version provided.", id) + } + default: + dbMasks = append(dbMasks, maskManager.Translate(masks)...) + if len(dbMasks) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cl.PublicId = id + out, rowsUpdated, err = repo.UpdateCredentialLibrary(ctx, projId, cl, item.GetVersion(), dbMasks) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Credential Library %q doesn't exist or incorrect version provided.", id) + } } return out, nil } @@ -402,7 +511,13 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, if err != nil { return false, err } - rows, err := repo.DeleteCredentialLibrary(ctx, scopeId, id) + rows := 0 + switch subtypes.SubtypeFromId(domain, id) { + case vault.SSHCertificateLibrarySubtype: + rows, err = repo.DeleteSSHCertificateCredentialLibrary(ctx, scopeId, id) + default: + rows, err = repo.DeleteCredentialLibrary(ctx, scopeId, id) + } if err != nil { if errors.IsNotFoundError(err) { return false, nil @@ -428,9 +543,9 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. parentId = id default: opts = append(opts, auth.WithId(id)) - + parentId = id switch subtypes.SubtypeFromId(domain, id) { - case vault.Subtype: + case vault.GenericLibrarySubtype: cl, err := repo.LookupCredentialLibrary(ctx, id) if err != nil { res.Error = err @@ -441,6 +556,17 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return res } parentId = cl.GetStoreId() + case vault.SSHCertificateLibrarySubtype: + cl, err := repo.LookupSSHCertificateCredentialLibrary(ctx, id) + if err != nil { + res.Error = err + return res + } + if cl == nil { + res.Error = handlers.NotFoundError() + return res + } + parentId = cl.GetStoreId() default: res.Error = errors.New(ctx, errors.InvalidParameter, op, "unrecognized credential library subtype from id") return res @@ -515,7 +641,7 @@ func toProto(in credential.Library, opt ...handlers.Option) (*pb.CredentialLibra out.AuthorizedActions = opts.WithAuthorizedActions } switch subtypes.SubtypeFromId(domain, in.GetPublicId()) { - case vault.Subtype: + case vault.GenericLibrarySubtype: vaultIn, ok := in.(*vault.CredentialLibrary) if !ok { return nil, errors.NewDeprecated(errors.Internal, op, "unable to cast to vault credential library") @@ -568,6 +694,47 @@ func toProto(in credential.Library, opt ...handlers.Option) (*pb.CredentialLibra VaultCredentialLibraryAttributes: attrs, } } + case vault.SSHCertificateLibrarySubtype: + vaultIn, ok := in.(*vault.SSHCertificateCredentialLibrary) + if !ok { + return nil, errors.NewDeprecated(errors.Internal, op, "unable to cast to vault ssh certificate credential library") + } + // We don't check for mapping overrides here -- this subtype does not currently support them. + out.CredentialType = vaultIn.GetCredentialType() + if outputFields.Has(globals.AttributesField) { + attrs := &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String(vaultIn.GetVaultPath()), + } + if vaultIn.GetUsername() != "" { + attrs.Username = wrapperspb.String(vaultIn.GetUsername()) + } + if vaultIn.GetKeyType() != "" { + attrs.KeyType = wrapperspb.String(vaultIn.GetKeyType()) + } + if vaultIn.GetKeyBits() != 0 { + attrs.KeyBits = &wrapperspb.UInt32Value{Value: vaultIn.GetKeyBits()} + } + if vaultIn.GetTtl() != "" { + attrs.Ttl = wrapperspb.String(vaultIn.GetTtl()) + } + if vaultIn.GetKeyId() != "" { + attrs.KeyId = wrapperspb.String(vaultIn.GetKeyId()) + } + if vaultIn.GetCriticalOptions() != "" { + co := make(map[string]string) + json.Unmarshal([]byte(vaultIn.GetCriticalOptions()), &co) + attrs.CriticalOptions = co + } + if vaultIn.GetExtensions() != "" { + e := make(map[string]string) + json.Unmarshal([]byte(vaultIn.GetExtensions()), &e) + attrs.Extensions = e + } + out.Attrs = &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: attrs, + } + } + } return &out, nil } @@ -582,7 +749,11 @@ func toStorageVaultLibrary(storeId string, in *pb.CredentialLibrary) (out *vault opts = append(opts, vault.WithDescription(in.GetDescription().GetValue())) } - attrs := in.GetVaultCredentialLibraryAttributes() + attrs := in.GetVaultGenericCredentialLibraryAttributes() + if attrs == nil { + // fallback to attributes for older subtype + attrs = in.GetVaultCredentialLibraryAttributes() + } if attrs.GetHttpMethod() != nil { opts = append(opts, vault.WithMethod(vault.Method(strings.ToUpper(attrs.GetHttpMethod().GetValue())))) } @@ -631,13 +802,66 @@ func toStorageVaultLibrary(storeId string, in *pb.CredentialLibrary) (out *vault return cs, err } +func toStorageVaultSSHCertificateLibrary(storeId string, in *pb.CredentialLibrary) (out *vault.SSHCertificateCredentialLibrary, err error) { + const op = "credentiallibraries.toStorageVaultSSHCertificateLibrary" + var opts []vault.Option + if in.GetName() != nil { + opts = append(opts, vault.WithName(in.GetName().GetValue())) + } + if in.GetDescription() != nil { + opts = append(opts, vault.WithDescription(in.GetDescription().GetValue())) + } + opts = append(opts, vault.WithCredentialType(credential.Type(in.GetCredentialType()))) + + attrs := in.GetVaultSshCertificateCredentialLibraryAttributes() + if attrs.GetKeyType() != nil { + opts = append(opts, vault.WithKeyType(attrs.GetKeyType().GetValue())) + } + if attrs.GetKeyBits() != nil { + opts = append(opts, vault.WithKeyBits(attrs.GetKeyBits().GetValue())) + } + if attrs.GetTtl() != nil { + opts = append(opts, vault.WithTtl(attrs.GetTtl().GetValue())) + } + if attrs.GetKeyId() != nil { + opts = append(opts, vault.WithKeyId(attrs.GetKeyId().GetValue())) + } + if attrs.GetCriticalOptions() != nil { + co, err := json.Marshal(attrs.GetCriticalOptions()) + if err != nil { + return nil, err + } + opts = append(opts, vault.WithCriticalOptions(string(co))) + } + if attrs.GetExtensions() != nil { + e, err := json.Marshal(attrs.GetExtensions()) + if err != nil { + return nil, err + } + opts = append(opts, vault.WithExtensions(string(e))) + } + + cs, err := vault.NewSSHCertificateCredentialLibrary(storeId, attrs.GetPath().GetValue(), attrs.GetUsername().GetValue(), opts...) + if err != nil { + return nil, errors.WrapDeprecated(err, op, errors.WithMsg("unable to build credential library")) + } + return cs, err +} + // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce // requirements on the structure of the request. They verify that: // - The path passed in is correctly formatted // - All required parameters are set // - There are no conflicting parameters provided func validateGetRequest(req *pbs.GetCredentialLibraryRequest) error { - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, vault.CredentialLibraryPrefix) + prefix := "" + switch subtypes.SubtypeFromId(domain, req.GetId()) { + case vault.SSHCertificateLibrarySubtype: + prefix = vault.SSHCertificateCredentialLibraryPrefix + default: + prefix = vault.CredentialLibraryPrefix + } + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, prefix) } func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { @@ -645,23 +869,82 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { badFields := map[string]string{} switch subtypes.SubtypeFromId(domain, req.GetItem().GetCredentialStoreId()) { case vault.Subtype: - if t := req.GetItem().GetType(); t != "" && subtypes.SubtypeFromType(domain, t) != vault.Subtype { + t := req.GetItem().GetType() + if t == "" { + badFields[globals.TypeField] = "This is a required field." + } + + // support older vault generic libraries with 'vault.Subtype'. + if subtypes.SubtypeFromType(domain, t) == vault.Subtype { + req.GetItem().Type = vault.GenericLibrarySubtype.String() + t = vault.GenericLibrarySubtype.String() + } + + if subtypes.SubtypeFromType(domain, t) != vault.GenericLibrarySubtype && + subtypes.SubtypeFromType(domain, t) != vault.SSHCertificateLibrarySubtype { badFields[globals.CredentialStoreIdField] = "If included, type must match that of the credential store." } - attrs := req.GetItem().GetVaultCredentialLibraryAttributes() - if attrs == nil { - badFields[attributesPathField] = "This is a required field." + + switch subtypes.SubtypeFromType(domain, req.GetItem().GetType()) { + case vault.GenericLibrarySubtype: + isValidCred := false + ct := req.GetItem().GetCredentialType() + for _, t := range validCredentialTypesVaultGeneric { + if ct == "" || ct == string(t) { + isValidCred = true + break + } + } + if !isValidCred { + badFields[globals.CredentialTypeField] = fmt.Sprintf("Unknown credential type %q", ct) + } + + attrs := req.GetItem().GetVaultGenericCredentialLibraryAttributes() + if attrs == nil { + // fallback to attributes for older subtype + attrs = req.GetItem().GetVaultCredentialLibraryAttributes() + if attrs == nil { + badFields[attributesPathField] = "This is a required field." + } + } + if attrs.GetPath().GetValue() == "" { + badFields[vaultPathField] = "This is a required field." + } + if m := attrs.GetHttpMethod(); m != nil && !strutil.StrListContains([]string{"GET", "POST"}, strings.ToUpper(m.GetValue())) { + badFields[httpMethodField] = "If set, value must be 'GET' or 'POST'." + } + if b := attrs.GetHttpRequestBody(); b != nil && strings.ToUpper(attrs.GetHttpMethod().GetValue()) != "POST" { + badFields[httpRequestBodyField] = fmt.Sprintf("Field can only be set if %q is set to the value 'POST'.", httpMethodField) + } + validateMapping(badFields, credential.Type(req.GetItem().GetCredentialType()), req.GetItem().CredentialMappingOverrides.AsMap()) + case vault.SSHCertificateLibrarySubtype: + if req.GetItem().GetCredentialType() != "" { + badFields[globals.CredentialTypeField] = fmt.Sprintf("This field is read only and cannot be set.") + } + + attrs := req.GetItem().GetVaultSshCertificateCredentialLibraryAttributes() + if attrs == nil { + badFields[attributesPathField] = "This is a required field." + } + if attrs.GetPath().GetValue() == "" { + badFields[vaultPathField] = "This is a required field." + } + if attrs.GetUsername().GetValue() == "" { + badFields[sshCertUsernameField] = "This is a required field." + } + if (attrs.GetKeyType() == nil) != (attrs.GetKeyBits() == nil) { + if attrs.GetKeyType() != nil && attrs.GetKeyType().GetValue() != vault.KeyTypeEd25519 { + badFields[keyTypeField] = fmt.Sprintf("If set, %q must also be set.", keyBitsField) + } + if attrs.GetKeyBits() != nil { + badFields[keyBitsField] = fmt.Sprintf("If set, %q must also be set.", keyTypeField) + } + } + if t := attrs.GetKeyType(); t != nil && !strutil.StrListContains(validKeyTypes, strings.ToLower(t.GetValue())) { + badFields[keyTypeField] = "If set, value must be 'ed25519', 'ecdsa', or 'rsa'." + } + validateKeyBits(badFields, attrs.GetKeyBits().GetValue(), attrs.GetKeyType().GetValue()) } - if attrs.GetPath().GetValue() == "" { - badFields[vaultPathField] = "This is a required field." - } - if m := attrs.GetHttpMethod(); m != nil && !strutil.StrListContains([]string{"GET", "POST"}, strings.ToUpper(m.GetValue())) { - badFields[httpMethodField] = "If set, value must be 'GET' or 'POST'." - } - if b := attrs.GetHttpRequestBody(); b != nil && strings.ToUpper(attrs.GetHttpMethod().GetValue()) != "POST" { - badFields[httpRequestBodyField] = fmt.Sprintf("Field can only be set if %q is set to the value 'POST'.", httpMethodField) - } - validateMapping(badFields, credential.Type(req.GetItem().GetCredentialType()), req.GetItem().CredentialMappingOverrides.AsMap()) default: badFields[globals.CredentialStoreIdField] = "This field must be a valid credential store id." } @@ -670,17 +953,29 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { } func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest, currentCredentialType credential.Type) error { + prefix := "" + st := subtypes.SubtypeFromId(domain, req.GetId()) + switch st { + case vault.GenericLibrarySubtype: + prefix = vault.CredentialLibraryPrefix + case vault.SSHCertificateLibrarySubtype: + prefix = vault.SSHCertificateCredentialLibraryPrefix + } return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} - switch subtypes.SubtypeFromId(domain, req.GetId()) { - case vault.Subtype: - if req.GetItem().GetType() != "" && subtypes.SubtypeFromType(domain, req.GetItem().GetType()) != vault.Subtype { + switch st { + case vault.GenericLibrarySubtype: + if req.GetItem().GetType() != "" && subtypes.SubtypeFromType(domain, req.GetItem().GetType()) != vault.GenericLibrarySubtype { badFields[globals.TypeField] = "Cannot modify resource type." } if req.GetItem().GetCredentialType() != "" && req.GetItem().GetCredentialType() != string(currentCredentialType) { badFields[globals.CredentialTypeField] = "Cannot modify credential type." } - attrs := req.GetItem().GetVaultCredentialLibraryAttributes() + attrs := req.GetItem().GetVaultGenericCredentialLibraryAttributes() + if attrs == nil { + // fallback to attributes for older subtype + attrs = req.GetItem().GetVaultCredentialLibraryAttributes() + } if attrs != nil { if handlers.MaskContains(req.GetUpdateMask().GetPaths(), vaultPathField) && attrs.GetPath().GetValue() == "" { badFields[vaultPathField] = "This is a required field and cannot be set to empty." @@ -693,13 +988,33 @@ func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest, currentCrede } validateMapping(badFields, currentCredentialType, req.GetItem().CredentialMappingOverrides.AsMap()) } + case vault.SSHCertificateLibrarySubtype: + if req.GetItem().GetType() != "" && subtypes.SubtypeFromType(domain, req.GetItem().GetType()) != vault.SSHCertificateLibrarySubtype { + badFields[globals.TypeField] = "Cannot modify resource type." + } + if req.GetItem().GetCredentialType() != "" && req.GetItem().GetCredentialType() != string(currentCredentialType) { + badFields[globals.CredentialTypeField] = "Cannot modify credential type." + } + attrs := req.GetItem().GetVaultSshCertificateCredentialLibraryAttributes() + if attrs != nil { + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), vaultPathField) && attrs.GetPath().GetValue() == "" { + badFields[vaultPathField] = "This is a required field and cannot be set to empty." + } + if u := attrs.GetUsername().GetValue(); handlers.MaskContains(req.GetUpdateMask().GetPaths(), sshCertUsernameField) && u == "" { + badFields[sshCertUsernameField] = "This is a required field and cannot be set to empty." + } + if t := attrs.GetKeyType(); t != nil && !strutil.StrListContains(validKeyTypes, strings.ToLower(t.GetValue())) { + badFields[keyTypeField] = "If set, value must be 'ed25519', 'ecdsa', or 'rsa'." + } + validateKeyBits(badFields, attrs.GetKeyBits().GetValue(), attrs.GetKeyType().GetValue()) + } } return badFields - }, vault.CredentialLibraryPrefix) + }, prefix) } func validateDeleteRequest(req *pbs.DeleteCredentialLibraryRequest) error { - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, vault.CredentialLibraryPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, vault.CredentialLibraryPrefix, vault.SSHCertificateCredentialLibraryPrefix) } func validateListRequest(req *pbs.ListCredentialLibrariesRequest) error { @@ -747,6 +1062,38 @@ func validateMapping(badFields map[string]string, credentialType credential.Type } } +// validateKeyBits appends to badFields if keyBits and keyType aren't accepted combinations for an SSHCertificateCredentialLibrary. +// If keyType is an empty string, validateKeyBits only validates keyBits. +func validateKeyBits(badFields map[string]string, keyBits uint32, keyType string) { + switch keyBits { + case vault.KeyBitsDefault: + case vault.KeyBitsEcdsa256, vault.KeyBitsEcdsa384, vault.KeyBitsEcdsa521: + if keyType != "" && keyType != vault.KeyTypeEcdsa { + badFields[keyBitsField] = fmt.Sprintf("Invalid bit size %d for key type %s", keyBits, keyType) + } + case vault.KeyBitsRsa2048, vault.KeyBitsRsa3072, vault.KeyBitsRsa4096: + if keyType != "" && keyType != vault.KeyTypeRsa { + badFields[keyBitsField] = fmt.Sprintf("Invalid bit size %d for key type %s", keyBits, keyType) + } + default: + badFields[keyBitsField] = fmt.Sprintf("Invalid bit size %d", keyBits) + } +} + +func getMapUpdate(field string, apiMasks []string) bool { + for _, m := range apiMasks { + if m == field { + return true + } + + fieldPrefix := fmt.Sprintf("%v.", field) + if s := strings.SplitN(m, fieldPrefix, 2); len(s) == 2 { + return true + } + } + return false +} + func getMappingUpdates(credentialType credential.Type, current vault.MappingOverride, new map[string]any, apiMasks []string) (map[string]any, bool) { ret := make(map[string]any) masks := make(map[string]bool) diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index 37c05350c..e1192e981 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -2,6 +2,7 @@ package credentiallibraries import ( "context" + "encoding/json" "fmt" "sort" "strings" @@ -65,7 +66,7 @@ func TestList(t *testing.T) { CreatedTime: l.GetCreateTime().GetTimestamp(), UpdatedTime: l.GetUpdateTime().GetTimestamp(), Version: l.GetVersion(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), AuthorizedActions: testAuthorizedActions, Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ @@ -75,7 +76,26 @@ func TestList(t *testing.T) { }, }) } - + for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 10) { + wantLibraries = append(wantLibraries, &pb.CredentialLibrary{ + Id: l.GetPublicId(), + CredentialStoreId: l.GetStoreId(), + CredentialType: l.GetCredentialType(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + CreatedTime: l.GetCreateTime().GetTimestamp(), + UpdatedTime: l.GetUpdateTime().GetTimestamp(), + Version: l.GetVersion(), + Type: vault.SSHCertificateLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(l.GetKeyType()), + Path: wrapperspb.String(l.GetVaultPath()), + Username: wrapperspb.String(l.GetUsername()), + }, + }, + }) + } cases := []struct { name string req *pbs.ListCredentialLibrariesRequest @@ -101,12 +121,6 @@ func TestList(t *testing.T) { res: &pbs.ListCredentialLibrariesResponse{Items: wantLibraries[1:2]}, anonRes: &pbs.ListCredentialLibrariesResponse{Items: wantLibraries[1:2]}, }, - { - name: "Filter on Attribute", - req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/path"==%q`, wantLibraries[2].GetVaultCredentialLibraryAttributes().GetPath().Value)}, - res: &pbs.ListCredentialLibrariesResponse{Items: wantLibraries[2:3]}, - anonRes: &pbs.ListCredentialLibrariesResponse{}, // anonymous user does not have access to attributes - }, { name: "Filter to No Libraries", req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: store.GetPublicId(), Filter: `"/item/id"=="doesnt match"`}, @@ -154,6 +168,119 @@ func TestList(t *testing.T) { } } +func TestList_Attributes(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repoFn := func() (*vault.Repository, error) { + return vault.NewRepository(rw, rw, kms, sche) + } + _, prj := iam.TestScopes(t, iamRepo) + + ts := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + storeGeneric, storeSSHCertificate := ts[0], ts[1] + var wantLibrariesGeneric []*pb.CredentialLibrary + var wantLibrariesSSHCertificate []*pb.CredentialLibrary + for _, l := range vault.TestCredentialLibraries(t, conn, wrapper, storeGeneric.GetPublicId(), 5) { + wantLibrariesGeneric = append(wantLibrariesGeneric, &pb.CredentialLibrary{ + Id: l.GetPublicId(), + CredentialStoreId: l.GetStoreId(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + CreatedTime: l.GetCreateTime().GetTimestamp(), + UpdatedTime: l.GetUpdateTime().GetTimestamp(), + Version: l.GetVersion(), + Type: vault.GenericLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(l.GetVaultPath()), + HttpMethod: wrapperspb.String(l.GetHttpMethod()), + }, + }, + }) + } + for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, storeSSHCertificate.GetPublicId(), 10) { + wantLibrariesSSHCertificate = append(wantLibrariesSSHCertificate, &pb.CredentialLibrary{ + Id: l.GetPublicId(), + CredentialStoreId: l.GetStoreId(), + CredentialType: l.GetCredentialType(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + CreatedTime: l.GetCreateTime().GetTimestamp(), + UpdatedTime: l.GetUpdateTime().GetTimestamp(), + Version: l.GetVersion(), + Type: vault.SSHCertificateLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(l.GetKeyType()), + Path: wrapperspb.String(l.GetVaultPath()), + Username: wrapperspb.String(l.GetUsername()), + }, + }, + }) + } + cases := []struct { + name string + req *pbs.ListCredentialLibrariesRequest + res *pbs.ListCredentialLibrariesResponse + anonRes *pbs.ListCredentialLibrariesResponse + err error + }{ + { + name: "Filter on Attribute Generic Library", + req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: storeGeneric.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/path"==%q`, wantLibrariesGeneric[2].GetVaultCredentialLibraryAttributes().GetPath().Value)}, + res: &pbs.ListCredentialLibrariesResponse{Items: wantLibrariesGeneric[2:3]}, + anonRes: &pbs.ListCredentialLibrariesResponse{}, // anonymous user does not have access to attributes + }, + { + name: "Filter on Attribute SSH Certificate Library", + req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: storeSSHCertificate.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/path"==%q`, wantLibrariesSSHCertificate[2].GetVaultSshCertificateCredentialLibraryAttributes().GetPath().Value)}, + res: &pbs.ListCredentialLibrariesResponse{Items: wantLibrariesSSHCertificate[2:3]}, + anonRes: &pbs.ListCredentialLibrariesResponse{}, // anonymous user does not have access to attributes + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s, err := NewService(repoFn, iamRepoFn) + require.NoError(t, err, "Couldn't create new host set service.") + + // Test non-anonymous listing + got, gErr := s.ListCredentialLibraries(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(t, gErr) + assert.True(t, errors.Is(gErr, tc.err), "ListCredentialLibrary(%q) got error %v, wanted %v", tc.req, gErr, tc.err) + return + } + require.NoError(t, gErr) + sort.Slice(got.Items, func(i, j int) bool { + return got.Items[i].GetId() < got.Items[j].GetId() + }) + want := proto.Clone(tc.res).(*pbs.ListCredentialLibrariesResponse) + sort.Slice(want.Items, func(i, j int) bool { + return want.Items[i].GetId() < want.Items[j].GetId() + }) + assert.Empty(t, cmp.Diff(got, want, protocmp.Transform())) + + // Test anonymous listing + got, gErr = s.ListCredentialLibraries(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) + require.NoError(t, gErr) + assert.Len(t, got.Items, len(tc.anonRes.Items)) + for _, item := range got.GetItems() { + require.Nil(t, item.CreatedTime) + require.Nil(t, item.UpdatedTime) + require.Zero(t, item.Version) + } + }) + } +} + func TestCreate(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -186,6 +313,7 @@ func TestCreate(t *testing.T) { name: "missing vault path", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{}, }, @@ -197,6 +325,7 @@ func TestCreate(t *testing.T) { name: "Can't specify Id", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Id: vault.CredentialLibraryPrefix + "_notallowed", Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ @@ -211,6 +340,7 @@ func TestCreate(t *testing.T) { name: "Can't specify Created Time", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), CreatedTime: timestamppb.Now(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ @@ -225,6 +355,7 @@ func TestCreate(t *testing.T) { name: "Can't specify Update Time", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), UpdatedTime: timestamppb.Now(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ @@ -252,7 +383,7 @@ func TestCreate(t *testing.T) { { name: "parent id must be included", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -266,6 +397,7 @@ func TestCreate(t *testing.T) { name: "Cannot specify a http request body when http method is get", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -280,6 +412,7 @@ func TestCreate(t *testing.T) { name: "Invalid Method", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -294,6 +427,7 @@ func TestCreate(t *testing.T) { name: "Request Body With GET Method", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -308,6 +442,7 @@ func TestCreate(t *testing.T) { name: "Invalid credential type", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -322,6 +457,7 @@ func TestCreate(t *testing.T) { name: "Invalid username_password mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -345,6 +481,7 @@ func TestCreate(t *testing.T) { name: "Using POST method", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -363,7 +500,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -379,6 +516,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ @@ -399,7 +537,7 @@ func TestCreate(t *testing.T) { Description: &wrapperspb.StringValue{Value: "desc"}, Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -414,6 +552,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary username_password type", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -431,7 +570,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -447,6 +586,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary username_password type with username mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -472,7 +612,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -496,6 +636,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary username_password type with username/password mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -522,7 +663,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -547,6 +688,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary ssh_private_key type", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -564,7 +706,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -580,6 +722,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary ssh_private_key type with mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -607,7 +750,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -629,6 +772,38 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "Create a valid vault CredentialLibrary with the 'vault' subtype", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.Subtype.String(), + Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }, + }, + }}, + idPrefix: vault.CredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.CredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.GenericLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + HttpMethod: wrapperspb.String("GET"), + }, + }, + AuthorizedActions: testAuthorizedActions, + }, + }, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -717,6 +892,11 @@ func TestGet(t *testing.T) { sshPkLib, err := repo.CreateCredentialLibrary(context.Background(), prj.GetPublicId(), lib1) require.NoError(t, err) + lib2, err := vault.NewSSHCertificateCredentialLibrary(store.GetPublicId(), "/ssh/sign/foo", "username") + require.NoError(t, err) + sshCertLib, err := repo.CreateSSHCertificateCredentialLibrary(context.Background(), prj.GetPublicId(), lib2) + require.NoError(t, err) + cases := []struct { name string id string @@ -731,7 +911,7 @@ func TestGet(t *testing.T) { Id: unspecifiedLib.GetPublicId(), CredentialStoreId: unspecifiedLib.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: unspecifiedLib.CreateTime.GetTimestamp(), UpdatedTime: unspecifiedLib.UpdateTime.GetTimestamp(), @@ -753,7 +933,7 @@ func TestGet(t *testing.T) { Id: userPassLib.GetPublicId(), CredentialStoreId: userPassLib.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: userPassLib.CreateTime.GetTimestamp(), UpdatedTime: userPassLib.UpdateTime.GetTimestamp(), @@ -785,7 +965,7 @@ func TestGet(t *testing.T) { Id: sshPkLib.GetPublicId(), CredentialStoreId: sshPkLib.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: sshPkLib.CreateTime.GetTimestamp(), UpdatedTime: sshPkLib.UpdateTime.GetTimestamp(), @@ -810,6 +990,30 @@ func TestGet(t *testing.T) { }, }, }, + { + name: "success-ssh-certificate", + id: sshCertLib.GetPublicId(), + res: &pbs.GetCredentialLibraryResponse{ + Item: &pb.CredentialLibrary{ + Id: sshCertLib.GetPublicId(), + CredentialStoreId: sshCertLib.GetStoreId(), + CredentialType: sshCertLib.GetCredentialType(), + Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + Type: vault.SSHCertificateLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + CreatedTime: sshCertLib.CreateTime.GetTimestamp(), + UpdatedTime: sshCertLib.UpdateTime.GetTimestamp(), + Version: 1, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(sshCertLib.GetKeyType()), + Path: wrapperspb.String(sshCertLib.GetVaultPath()), + Username: wrapperspb.String(sshCertLib.GetUsername()), + }, + }, + }, + }, + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", vault.CredentialLibraryPrefix), @@ -863,6 +1067,7 @@ func TestDelete(t *testing.T) { store := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] vl := vault.TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] + vl2 := vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] s, err := NewService(repoFn, iamRepoFn) require.NoError(t, err) @@ -876,6 +1081,10 @@ func TestDelete(t *testing.T) { name: "success", id: vl.GetPublicId(), }, + { + name: "success-ssh-cert", + id: vl2.GetPublicId(), + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", vault.CredentialLibraryPrefix), @@ -1673,3 +1882,668 @@ func TestUpdate(t *testing.T) { assert.Nil(t, cl.GetItem().GetVaultCredentialLibraryAttributes().GetHttpRequestBody()) }) } + +func TestCreate_SSHCertificateCredentialLibrary(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repoFn := func() (*vault.Repository, error) { + return vault.NewRepository(rw, rw, kms, sche) + } + + _, prj := iam.TestScopes(t, iamRepo) + store := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + defaultCL := vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] + defaultCreated := defaultCL.GetCreateTime().GetTimestamp() + + cases := []struct { + name string + req *pbs.CreateCredentialLibraryRequest + res *pbs.CreateCredentialLibraryResponse + idPrefix string + err error + wantErr bool + }{ + { + name: "missing vault path", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Username: wrapperspb.String("username"), + }, + }, + }}, + idPrefix: vault.SSHCertificateCredentialLibraryPrefix + "_", + wantErr: true, + }, + { + name: "missing username", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }, + }, + }}, + idPrefix: vault.SSHCertificateCredentialLibraryPrefix + "_", + wantErr: true, + }, + { + name: "Can't specify Id", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Id: vault.SSHCertificateCredentialLibraryPrefix + "_notallowed", + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Created Time", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + CreatedTime: timestamppb.Now(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Update Time", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + UpdatedTime: timestamppb.Now(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Type and parent id must match", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: "static", + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "parent id must be included", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid credential type", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + CredentialType: "fake-type", + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid key type key bits combination", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + KeyType: wrapperspb.String(vault.KeyTypeEd25519), + KeyBits: wrapperspb.UInt32(256), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Create a valid vault SSHCertificateCredentialLibrary", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/sign/foo"), + Username: wrapperspb.String("username"), + }, + }, + }}, + idPrefix: vault.SSHCertificateCredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.SSHCertificateCredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/sign/foo"), + Username: wrapperspb.String("username"), + KeyType: wrapperspb.String(vault.KeyTypeEd25519), + }, + }, + AuthorizedActions: testAuthorizedActions, + CredentialType: string(credential.SshCertificateType), + }, + }, + }, + { + name: "Seting key type ed25119 with key bits nil", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Name: &wrapperspb.StringValue{Value: "name2"}, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/sign/foo"), + Username: wrapperspb.String("username"), + KeyType: wrapperspb.String(vault.KeyTypeEd25519), + }, + }, + }}, + idPrefix: vault.SSHCertificateCredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.SSHCertificateCredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Name: &wrapperspb.StringValue{Value: "name2"}, + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/sign/foo"), + Username: wrapperspb.String("username"), + KeyType: wrapperspb.String(vault.KeyTypeEd25519), + }, + }, + AuthorizedActions: testAuthorizedActions, + CredentialType: string(credential.SshCertificateType), + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + s, err := NewService(repoFn, iamRepoFn) + require.NoError(err, "Error when getting new credential store service.") + + got, gErr := s.CreateCredentialLibrary(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), tc.req) + if tc.wantErr || tc.err != nil { + require.Error(gErr) + if tc.err != nil { + assert.True(errors.Is(gErr, tc.err), "CreateCredentialLibrary(...) got error %v, wanted %v", gErr, tc.err) + } + return + } + require.NoError(gErr) + if tc.res == nil { + require.Nil(got) + } + if got != nil { + assert.Contains(got.GetUri(), tc.res.Uri) + assert.True(strings.HasPrefix(got.GetItem().GetId(), tc.idPrefix)) + gotCreateTime := got.GetItem().GetCreatedTime() + gotUpdateTime := got.GetItem().GetUpdatedTime() + + // Verify it is a credential store created after the test setup's default credential store + assert.True(gotCreateTime.AsTime().After(defaultCreated.AsTime()), "New credential store should have been created after default credential store. Was created %v, which is after %v", gotCreateTime, defaultCreated) + assert.True(gotUpdateTime.AsTime().After(defaultCreated.AsTime()), "New credential store should have been updated after default credential store. Was updated %v, which is after %v", gotUpdateTime, defaultCreated) + + // Clear all values which are hard to compare against. + got.Uri, tc.res.Uri = "", "" + got.Item.Id, tc.res.Item.Id = "", "" + got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "CreateCredentialLibrary(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdate_SSHCertificateCredentialLibrary(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repoFn := func() (*vault.Repository, error) { + return vault.NewRepository(rw, rw, kms, sche) + } + + _, prj := iam.TestScopes(t, iamRepo) + ctx := auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()) + + s, err := NewService(repoFn, iamRepoFn) + require.NoError(t, err) + cs := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + store, diffStore := cs[0], cs[1] + + freshLibrary := func(opt ...vault.Option) (*vault.SSHCertificateCredentialLibrary, func()) { + repo, err := repoFn() + require.NoError(t, err) + lib, err := vault.NewSSHCertificateCredentialLibrary(store.GetPublicId(), "/ssh/sign/foo", "username", opt...) + require.NoError(t, err) + + vl, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), lib) + require.NoError(t, err) + clean := func() { + _, err := s.DeleteCredentialLibrary(ctx, &pbs.DeleteCredentialLibraryRequest{Id: vl.GetPublicId()}) + require.NoError(t, err) + } + return vl, clean + } + + fieldmask := func(paths ...string) *fieldmaskpb.FieldMask { + return &fieldmaskpb.FieldMask{Paths: paths} + } + + v := vault.NewTestVaultServer(t, vault.WithTestVaultTLS(vault.TestClientTLS)) + _, token := v.CreateToken(t) + _ = token + + testExtensionBytes, _ := json.Marshal(map[string]string{"permit-pty": ""}) + testCriticalOptionsBytes, _ := json.Marshal(map[string]string{"option-a": "set-a"}) + + successCases := []struct { + name string + opts []vault.Option + req *pbs.UpdateCredentialLibraryRequest + res func(*pb.CredentialLibrary) *pb.CredentialLibrary + }{ + { + name: "name and description", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("name", "description"), + Item: &pb.CredentialLibrary{ + Name: wrapperspb.String("basic"), + Description: wrapperspb.String("basic"), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.Name = wrapperspb.String("basic") + out.Description = wrapperspb.String("basic") + return out + }, + }, + { + name: "update username", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(sshCertUsernameField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Username: wrapperspb.String("changed-username"), + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().Username = wrapperspb.String("changed-username") + return out + }, + }, + { + name: "update key type and key bits", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.key_type", "attributes.key_bits"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(vault.KeyTypeRsa), + KeyBits: wrapperspb.UInt32(2048), + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().KeyType = wrapperspb.String("rsa") + out.GetVaultSshCertificateCredentialLibraryAttributes().KeyBits = wrapperspb.UInt32(2048) + return out + }, + }, + { + name: "update path", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(vaultPathField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/issue/foo"), + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().Path = wrapperspb.String("/ssh/issue/foo") + return out + }, + }, + { + name: "update other attr fields", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.ttl", "attributes.key_id", "attributes.critical_options.some", "attributes.extensions.permity-pty"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyId: wrapperspb.String("id"), + CriticalOptions: map[string]string{"some": "option"}, + Extensions: map[string]string{"permit-pty": ""}, + Ttl: wrapperspb.String("2m"), + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().Ttl = wrapperspb.String("2m") + out.GetVaultSshCertificateCredentialLibraryAttributes().KeyId = wrapperspb.String("id") + out.GetVaultSshCertificateCredentialLibraryAttributes().CriticalOptions = map[string]string{"some": "option"} + out.GetVaultSshCertificateCredentialLibraryAttributes().Extensions = map[string]string{"permit-pty": ""} + return out + }, + }, + { + name: "clear critical options, extensions fields", + opts: []vault.Option{vault.WithExtensions(string(testExtensionBytes)), vault.WithCriticalOptions(string(testCriticalOptionsBytes))}, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.critical_options", "attributes.extensions.permit-pty"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + CriticalOptions: nil, + Extensions: nil, + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().CriticalOptions = nil + out.GetVaultSshCertificateCredentialLibraryAttributes().Extensions = nil + return out + }, + }, + { + name: "set critical options while clearing extensions", + opts: []vault.Option{vault.WithExtensions(string(testExtensionBytes)), vault.WithCriticalOptions(string(testCriticalOptionsBytes))}, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.critical_options.option-b", "attributes.extensions"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + CriticalOptions: map[string]string{"option-b": "set-b"}, + Extensions: nil, + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().CriticalOptions = map[string]string{"option-b": "set-b"} + out.GetVaultSshCertificateCredentialLibraryAttributes().Extensions = nil + return out + }, + }, + { + name: "set multiple critical options, extensions", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.critical_options.option-a", "attributes.critical_options.option-b", "attributes.critical_options.option-c", + "attributes.extensions.permit-pty", "attributes.extensions.permit-port-forwarding", "attributes.extensions.permit-X11-forwarding"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + CriticalOptions: map[string]string{"option-a": "set-a", "option-b": "set-b", "option-c": "set-c"}, + Extensions: map[string]string{"permit-pty": "", "permit-port-forwarding": "", "permit-X11-forwarding": ""}, + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().CriticalOptions = map[string]string{"option-a": "set-a", "option-b": "set-b", "option-c": "set-c"} + out.GetVaultSshCertificateCredentialLibraryAttributes().Extensions = map[string]string{"permit-pty": "", "permit-port-forwarding": "", "permit-X11-forwarding": ""} + return out + }, + }, + } + + for _, tc := range successCases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + st, cleanup := freshLibrary(tc.opts...) + defer cleanup() + + if tc.req.Item.GetVersion() == 0 { + tc.req.Item.Version = 1 + } + if tc.req.GetId() == "" { + tc.req.Id = st.GetPublicId() + } + resToChange, err := s.GetCredentialLibrary(ctx, &pbs.GetCredentialLibraryRequest{Id: st.GetPublicId()}) + require.NoError(err) + want := &pbs.UpdateCredentialLibraryResponse{Item: tc.res(resToChange.GetItem())} + + got, gErr := s.UpdateCredentialLibrary(ctx, tc.req) + require.NoError(gErr) + require.NotNil(got) + + gotUpdateTime := got.GetItem().GetUpdatedTime() + created := st.GetCreateTime().GetTimestamp() + assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + + want.Item.UpdatedTime = got.Item.UpdatedTime + + assert.EqualValues(2, got.Item.Version) + want.Item.Version = 2 + + assert.Empty(cmp.Diff(got, want, protocmp.Transform())) + }) + } + + vl, cleanup := freshLibrary() + defer cleanup() + + errCases := []struct { + name string + path string + item *pb.CredentialLibrary + }{ + { + name: "read only type", + path: "type", + item: &pb.CredentialLibrary{Type: "something"}, + }, + { + name: "read only store_id", + path: "store_id", + item: &pb.CredentialLibrary{CredentialStoreId: diffStore.GetPublicId()}, + }, + { + name: "read only updated_time", + path: "updated_time", + item: &pb.CredentialLibrary{UpdatedTime: timestamppb.Now()}, + }, + { + name: "read only created_time", + path: "created_time", + item: &pb.CredentialLibrary{UpdatedTime: timestamppb.Now()}, + }, + { + name: "read only authorized actions", + path: "authorized actions", + item: &pb.CredentialLibrary{AuthorizedActions: append(testAuthorizedActions, "another")}, + }, + { + name: "read only credential type", + path: "credential_type", + item: &pb.CredentialLibrary{CredentialType: string(credential.UsernamePasswordType)}, + }, + } + for _, tc := range errCases { + t.Run(tc.name, func(t *testing.T) { + req := &pbs.UpdateCredentialLibraryRequest{ + Id: vl.GetPublicId(), + Item: tc.item, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}}, + } + req.Item.Version = vl.Version + + got, gErr := s.UpdateCredentialLibrary(ctx, req) + assert.Error(t, gErr) + assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr) + assert.Nil(t, got) + }) + } + vErrs := []struct { + name string + req *pbs.UpdateCredentialLibraryRequest + errContains string + }{ + { + name: "invalid key type key bits combination", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(keyBitsField, keyTypeField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(vault.KeyTypeEcdsa), + KeyBits: wrapperspb.UInt32(2048), + }, + }, + }, + }, + errContains: "Invalid bit size 2048 for key type ecdsa", + }, + { + name: "invalid key type", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(keyTypeField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String("unknown-type"), + }, + }, + }, + }, + errContains: "If set, value must be 'ed25519', 'ecdsa', or 'rsa'.", + }, + { + name: "invalid key bits", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(keyBitsField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyBits: wrapperspb.UInt32(1234), + }, + }, + }, + }, + errContains: "Invalid bit size 1234", + }, + } + for _, tc := range vErrs { + t.Run(tc.name, func(t *testing.T) { + scl, cleanup := freshLibrary() + defer cleanup() + + if tc.req.Item.GetVersion() == 0 { + tc.req.Item.Version = 1 + } + if tc.req.GetId() == "" { + tc.req.Id = scl.GetPublicId() + } + got, gErr := s.UpdateCredentialLibrary(ctx, tc.req) + if gErr != nil { + assert.Contains(t, gErr.Error(), tc.errContains) + assert.Nil(t, got) + } + }) + } +} 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 d48fe1287..63ec28b42 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -2516,6 +2516,7 @@ func TestAuthorizeSession(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("Library Name"), Description: wrapperspb.String("Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: &wrapperspb.StringValue{ @@ -2629,7 +2630,7 @@ func TestAuthorizeSession(t *testing.T) { Name: clsResp.GetItem().GetName().GetValue(), Description: clsResp.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), }, }}, // TODO: validate the contents of the authorization token is what is expected @@ -2758,6 +2759,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("Usernamepassword Library"), Description: wrapperspb.String("Usernamepassword Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "default-userpass")), @@ -2772,6 +2774,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("Unspecified Library"), Description: wrapperspb.String("Unspecified Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "default-userpass")), @@ -2789,6 +2792,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("Usernamepassword Mapping Library"), Description: wrapperspb.String("Usernamepassword Mapping Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "non-default-userpass")), @@ -2860,6 +2864,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("SSH Private Key Library"), Description: wrapperspb.String("SSH Private Key Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "default-sshpk")), @@ -2878,6 +2883,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("SSH Private Key Mapping Library"), Description: wrapperspb.String("SSH Private Key Mapping Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "non-default-sshpk")), @@ -2900,6 +2906,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("SSH Private Key With Passphrase Library"), Description: wrapperspb.String("SSH Private Key Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "default-sshpk-with-pass")), @@ -2919,6 +2926,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("SSH Private Key With Passphrase Mapping Library"), Description: wrapperspb.String("SSH Private Key Mapping Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "non-default-sshpk-with-pass")), @@ -2955,7 +2963,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespUnspecified.GetItem().GetName().GetValue(), Description: clsRespUnspecified.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), }, }, }, @@ -2971,7 +2979,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespUsernamePassword.GetItem().GetName().GetValue(), Description: clsRespUsernamePassword.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.UsernamePasswordType), }, Credential: func() *structpb.Struct { @@ -2997,7 +3005,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespUsernamePasswordWithMapping.GetItem().GetName().GetValue(), Description: clsRespUsernamePasswordWithMapping.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.UsernamePasswordType), }, Credential: func() *structpb.Struct { @@ -3075,7 +3083,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespSshPrivateKey.GetItem().GetName().GetValue(), Description: clsRespSshPrivateKey.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.SshPrivateKeyType), }, Credential: func() *structpb.Struct { @@ -3101,7 +3109,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespSshPrivateKeyWithMapping.GetItem().GetName().GetValue(), Description: clsRespSshPrivateKeyWithMapping.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.SshPrivateKeyType), }, Credential: func() *structpb.Struct { @@ -3154,7 +3162,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespSshPrivateKeyWithPass.GetItem().GetName().GetValue(), Description: clsRespSshPrivateKeyWithPass.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.SshPrivateKeyType), }, Credential: func() *structpb.Struct { @@ -3181,7 +3189,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespSshPrivateKeyWithPassWithMapping.GetItem().GetName().GetValue(), Description: clsRespSshPrivateKeyWithPassWithMapping.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.SshPrivateKeyType), }, Credential: func() *structpb.Struct { @@ -3392,6 +3400,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { clsResp, err := credService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), Description: wrapperspb.String(fmt.Sprintf("Library Description for target %q", tar.GetName())), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("database", "creds", "opened")), @@ -3416,6 +3425,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { clsResp, err := credService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), Description: wrapperspb.String(fmt.Sprintf("Library Description for target %q", tar.GetName())), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("bad path"), @@ -3440,6 +3450,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { 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())), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("database", "creds", "opened")), diff --git a/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto b/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto index e67589c53..fe6fb8826 100644 --- a/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto +++ b/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto @@ -17,10 +17,7 @@ message CredentialLibrary { string id = 10; // @gotags: `class:"public"` // The ID of the Credential Store of which this Credential Library is a part. - string credential_store_id = 20 [ - json_name = "credential_store_id", - (controller.custom_options.v1.subtype_source_id) = true - ]; // @gotags: `class:"public"` + string credential_store_id = 20 [json_name = "credential_store_id"]; // @gotags: `class:"public"` // Output only. Scope information for this Credential Library. resources.scopes.v1.ScopeInfo scope = 30; @@ -67,6 +64,16 @@ message CredentialLibrary { (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.subtype) = "vault" ]; + VaultSSHCertificateCredentialLibraryAttributes vault_ssh_certificate_credential_library_attributes = 102 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "vault-ssh-certificate" + ]; + VaultCredentialLibraryAttributes vault_generic_credential_library_attributes = 103 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "vault-generic" + ]; } // Output only. The available actions on this resource for this user. @@ -116,3 +123,78 @@ message VaultCredentialLibraryAttributes { } ]; // @gotags: `class:"secret"` } + +// The attributes of a vault SSH Certificate Credential Library. +message VaultSSHCertificateCredentialLibraryAttributes { + // The path in Vault to request credentials from. + google.protobuf.StringValue path = 10 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.path" + that: "VaultPath" + } + ]; // @gotags: `class:"public"` + + // The username to use when making an SSH connection. + google.protobuf.StringValue username = 20 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.username" + that: "Username" + } + ]; // @gotags: `class:"sensitive"` + + // The key type to use when generating an SSH private key. + google.protobuf.StringValue key_type = 30 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.key_type" + that: "KeyType" + } + ]; // @gotags: `class:"public"` + + // The number of bits to use to generate an SSH private key. + google.protobuf.UInt32Value key_bits = 40 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.key_bits" + that: "KeyBits" + } + ]; // @gotags: `class:"public"` + + // The requested time to live for the certificate. + google.protobuf.StringValue ttl = 50 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.ttl" + that: "Ttl" + } + ]; // @gotags: `class:"public"` + + // The key id that the created certificate should have. + google.protobuf.StringValue key_id = 60 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.key_id" + that: "KeyId" + } + ]; // @gotags: `class:"public"` + + // The critical options that the certificate should be signed for. + map critical_options = 70 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.critical_options" + that: "CriticalOptions" + } + ]; // @gotags: `class:"public"` + + // The extensions that the certificate should be signed for. + map extensions = 80 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.extensions" + that: "Extensions" + } + ]; // @gotags: `class:"public"` +} diff --git a/internal/tests/api/credentiallibraries/credentiallibrary_test.go b/internal/tests/api/credentiallibraries/credentiallibrary_test.go index 2016c92be..60a5684df 100644 --- a/internal/tests/api/credentiallibraries/credentiallibrary_test.go +++ b/internal/tests/api/credentiallibraries/credentiallibrary_test.go @@ -44,7 +44,7 @@ func TestList(t *testing.T) { expected = append(expected, &credentiallibraries.CredentialLibrary{Name: fmt.Sprint(i), Attributes: map[string]any{"vault_path": "something"}}) } - cl, err := lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName(expected[0].Name), credentiallibraries.WithVaultCredentialLibraryPath("something")) + cl, err := lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName(expected[0].Name), credentiallibraries.WithVaultCredentialLibraryPath("something")) require.NoError(err) expected[0] = cl.Item @@ -53,7 +53,7 @@ func TestList(t *testing.T) { assert.ElementsMatch(comparableSetSlice(expected[:1]), comparableSetSlice(ul.Items)) for i := 1; i < 10; i++ { - cl, err = lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName(expected[i].Name), credentiallibraries.WithVaultCredentialLibraryPath("something")) + cl, err = lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName(expected[i].Name), credentiallibraries.WithVaultCredentialLibraryPath("something")) require.NoError(err) expected[i] = cl.Item } @@ -115,8 +115,9 @@ func TestCrud(t *testing.T) { lClient := credentiallibraries.NewClient(client) - r, err := lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName("foo"), + r, err := lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName("foo"), credentiallibraries.WithVaultCredentialLibraryPath("something")) + require.Nil(err) checkResource(t, "create", r.Item, err, "foo", 1) r, err = lClient.Read(tc.Context(), r.Item.Id) @@ -157,7 +158,7 @@ func TestErrors(t *testing.T) { lClient := credentiallibraries.NewClient(client) - l, err := lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName("foo"), + l, err := lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName("foo"), credentiallibraries.WithVaultCredentialLibraryPath("something")) require.NoError(err) assert.NotNil(l) @@ -178,7 +179,7 @@ func TestErrors(t *testing.T) { assert.NotNil(apiErr) assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) - l, err = lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName("foo")) + l, err = lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName("foo")) require.Error(err) apiErr = api.AsServerError(err) assert.NotNil(apiErr) diff --git a/internal/tests/api/targets/target_test.go b/internal/tests/api/targets/target_test.go index 90685d7a3..dd4ee9a26 100644 --- a/internal/tests/api/targets/target_test.go +++ b/internal/tests/api/targets/target_test.go @@ -86,11 +86,11 @@ func TestCredentialSourcesASD(t *testing.T) { require.NotNil(csVault) lClient := credentiallibraries.NewClient(client) - lib1, err := lClient.Create(tc.Context(), csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something1")) + lib1, err := lClient.Create(tc.Context(), csVault.Item.Type, csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something1")) require.NoError(err) require.NotNil(lib1) - lib2, err := lClient.Create(tc.Context(), csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something2")) + lib2, err := lClient.Create(tc.Context(), csVault.Item.Type, csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something2")) require.NoError(err) require.NotNil(lib2) @@ -204,11 +204,11 @@ func TestDeprecatedCredentialSourcesASD(t *testing.T) { require.NotNil(csVault) lClient := credentiallibraries.NewClient(client) - lib1, err := lClient.Create(tc.Context(), csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something1")) + lib1, err := lClient.Create(tc.Context(), csVault.Item.Type, csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something1")) require.NoError(err) require.NotNil(lib1) - lib2, err := lClient.Create(tc.Context(), csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something2")) + lib2, err := lClient.Create(tc.Context(), csVault.Item.Type, csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something2")) require.NoError(err) require.NotNil(lib2) diff --git a/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go b/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go index db9d960b5..3d170f99f 100644 --- a/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go +++ b/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go @@ -55,6 +55,8 @@ type CredentialLibrary struct { // // *CredentialLibrary_Attributes // *CredentialLibrary_VaultCredentialLibraryAttributes + // *CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes + // *CredentialLibrary_VaultGenericCredentialLibraryAttributes Attrs isCredentialLibrary_Attrs `protobuf_oneof:"attrs"` // Output only. The available actions on this resource for this user. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty" class:"public"` // @gotags: `class:"public"` @@ -180,6 +182,20 @@ func (x *CredentialLibrary) GetVaultCredentialLibraryAttributes() *VaultCredenti return nil } +func (x *CredentialLibrary) GetVaultSshCertificateCredentialLibraryAttributes() *VaultSSHCertificateCredentialLibraryAttributes { + if x, ok := x.GetAttrs().(*CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes); ok { + return x.VaultSshCertificateCredentialLibraryAttributes + } + return nil +} + +func (x *CredentialLibrary) GetVaultGenericCredentialLibraryAttributes() *VaultCredentialLibraryAttributes { + if x, ok := x.GetAttrs().(*CredentialLibrary_VaultGenericCredentialLibraryAttributes); ok { + return x.VaultGenericCredentialLibraryAttributes + } + return nil +} + func (x *CredentialLibrary) GetAuthorizedActions() []string { if x != nil { return x.AuthorizedActions @@ -214,10 +230,23 @@ type CredentialLibrary_VaultCredentialLibraryAttributes struct { VaultCredentialLibraryAttributes *VaultCredentialLibraryAttributes `protobuf:"bytes,101,opt,name=vault_credential_library_attributes,json=vaultCredentialLibraryAttributes,proto3,oneof"` } +type CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes struct { + VaultSshCertificateCredentialLibraryAttributes *VaultSSHCertificateCredentialLibraryAttributes `protobuf:"bytes,102,opt,name=vault_ssh_certificate_credential_library_attributes,json=vaultSshCertificateCredentialLibraryAttributes,proto3,oneof"` +} + +type CredentialLibrary_VaultGenericCredentialLibraryAttributes struct { + VaultGenericCredentialLibraryAttributes *VaultCredentialLibraryAttributes `protobuf:"bytes,103,opt,name=vault_generic_credential_library_attributes,json=vaultGenericCredentialLibraryAttributes,proto3,oneof"` +} + func (*CredentialLibrary_Attributes) isCredentialLibrary_Attrs() {} func (*CredentialLibrary_VaultCredentialLibraryAttributes) isCredentialLibrary_Attrs() {} +func (*CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes) isCredentialLibrary_Attrs() { +} + +func (*CredentialLibrary_VaultGenericCredentialLibraryAttributes) isCredentialLibrary_Attrs() {} + // The attributes of a vault typed Credential Library. type VaultCredentialLibraryAttributes struct { state protoimpl.MessageState @@ -285,6 +314,118 @@ func (x *VaultCredentialLibraryAttributes) GetHttpRequestBody() *wrapperspb.Stri return nil } +// The attributes of a vault SSH Certificate Credential Library. +type VaultSSHCertificateCredentialLibraryAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The path in Vault to request credentials from. + Path *wrapperspb.StringValue `protobuf:"bytes,10,opt,name=path,proto3" json:"path,omitempty" class:"public"` // @gotags: `class:"public"` + // The username to use when making an SSH connection. + Username *wrapperspb.StringValue `protobuf:"bytes,20,opt,name=username,proto3" json:"username,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"` + // The key type to use when generating an SSH private key. + KeyType *wrapperspb.StringValue `protobuf:"bytes,30,opt,name=key_type,json=keyType,proto3" json:"key_type,omitempty" class:"public"` // @gotags: `class:"public"` + // The number of bits to use to generate an SSH private key. + KeyBits *wrapperspb.UInt32Value `protobuf:"bytes,40,opt,name=key_bits,json=keyBits,proto3" json:"key_bits,omitempty" class:"public"` // @gotags: `class:"public"` + // The requested time to live for the certificate. + Ttl *wrapperspb.StringValue `protobuf:"bytes,50,opt,name=ttl,proto3" json:"ttl,omitempty" class:"public"` // @gotags: `class:"public"` + // The key id that the created certificate should have. + KeyId *wrapperspb.StringValue `protobuf:"bytes,60,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" class:"public"` // @gotags: `class:"public"` + // The critical options that the certificate should be signed for. + CriticalOptions map[string]string `protobuf:"bytes,70,rep,name=critical_options,json=criticalOptions,proto3" json:"critical_options,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3" class:"public"` // @gotags: `class:"public"` + // The extensions that the certificate should be signed for. + Extensions map[string]string `protobuf:"bytes,80,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3" class:"public"` // @gotags: `class:"public"` +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) Reset() { + *x = VaultSSHCertificateCredentialLibraryAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VaultSSHCertificateCredentialLibraryAttributes) ProtoMessage() {} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VaultSSHCertificateCredentialLibraryAttributes.ProtoReflect.Descriptor instead. +func (*VaultSSHCertificateCredentialLibraryAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDescGZIP(), []int{2} +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) GetPath() *wrapperspb.StringValue { + if x != nil { + return x.Path + } + return nil +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) GetUsername() *wrapperspb.StringValue { + if x != nil { + return x.Username + } + return nil +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) GetKeyType() *wrapperspb.StringValue { + if x != nil { + return x.KeyType + } + return nil +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) GetKeyBits() *wrapperspb.UInt32Value { + if x != nil { + return x.KeyBits + } + return nil +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) GetTtl() *wrapperspb.StringValue { + if x != nil { + return x.Ttl + } + return nil +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) GetKeyId() *wrapperspb.StringValue { + if x != nil { + return x.KeyId + } + return nil +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) GetCriticalOptions() map[string]string { + if x != nil { + return x.CriticalOptions + } + return nil +} + +func (x *VaultSSHCertificateCredentialLibraryAttributes) GetExtensions() map[string]string { + if x != nil { + return x.Extensions + } + return nil +} + var File_controller_api_resources_credentiallibraries_v1_credential_library_proto protoreflect.FileDescriptor var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDesc = []byte{ @@ -309,99 +450,199 @@ var file_controller_api_resources_credentiallibraries_v1_credential_library_prot 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0xd9, 0x07, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x74, 0x6f, 0x22, 0xae, 0x0b, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x36, 0x0a, 0x13, 0x63, 0x72, 0x65, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, - 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xa0, 0xe3, 0x29, 0x01, 0x52, 0x13, 0x63, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, - 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x46, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x42, 0x14, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x62, - 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x42, 0x22, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x12, 0x4a, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x64, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x0f, 0xa0, - 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x00, - 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xc1, 0x01, 0x0a, - 0x23, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, - 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x51, 0x2e, 0x63, 0x6f, 0x6e, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, - 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x75, - 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, - 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x1d, 0xa0, - 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x05, 0x76, 0x61, 0x75, 0x6c, 0x74, 0xfa, 0xd2, 0xe4, 0x93, - 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x20, - 0x76, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, - 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x2f, 0x0a, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x18, 0xb6, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xa0, 0xda, 0x29, - 0x01, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x12, 0x62, 0x0a, 0x1c, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, - 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, - 0x65, 0x73, 0x18, 0xc0, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, - 0x63, 0x74, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x1c, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x6f, 0x76, 0x65, - 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, - 0xee, 0x02, 0x0a, 0x20, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x12, 0x46, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x14, 0xa0, 0xda, + 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x4e, 0x61, + 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x62, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x22, 0xa0, 0xda, 0x29, + 0x01, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x0c, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x3c, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x0c, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x46, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x5a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x4a, 0x0a, 0x0a, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x0f, 0xa0, 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, + 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xc1, 0x01, 0x0a, 0x23, 0x76, 0x61, 0x75, 0x6c, 0x74, + 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, + 0x61, 0x72, 0x79, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x65, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x51, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, + 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, + 0x69, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x1d, 0xa0, 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, + 0x05, 0x76, 0x61, 0x75, 0x6c, 0x74, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, + 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x20, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xfd, 0x01, 0x0a, 0x33, 0x76, + 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, + 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x5f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, + 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x75, 0x6c, 0x74, + 0x53, 0x53, 0x48, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x72, + 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x2d, 0xa0, 0xda, 0x29, 0x01, 0x9a, + 0xe3, 0x29, 0x15, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2d, 0x73, 0x73, 0x68, 0x2d, 0x63, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, + 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x2e, 0x76, 0x61, 0x75, 0x6c, + 0x74, 0x53, 0x73, 0x68, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xd8, 0x01, 0x0a, 0x2b, 0x76, + 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x5f, 0x63, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x51, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x42, 0x25, 0xa0, 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x0d, 0x76, 0x61, 0x75, + 0x6c, 0x74, 0x2d, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, + 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x27, 0x76, 0x61, + 0x75, 0x6c, 0x74, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2f, 0x0a, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0xb6, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x62, 0x0a, 0x1c, 0x63, 0x72, 0x65, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x6f, 0x76, + 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x18, 0xc0, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x1c, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, + 0x74, 0x74, 0x72, 0x73, 0x22, 0xee, 0x02, 0x0a, 0x20, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, + 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x24, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, + 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x74, 0x68, + 0x12, 0x09, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x12, 0x6c, 0x0a, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2c, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0a, 0x48, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x52, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, + 0x83, 0x01, 0x0a, 0x11, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x37, 0xa0, 0xda, 0x29, 0x01, 0xc2, + 0xdd, 0x29, 0x2f, 0x0a, 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, + 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, + 0x79, 0x12, 0x0f, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, + 0x64, 0x79, 0x52, 0x11, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x22, 0xf1, 0x08, 0x0a, 0x2e, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x53, + 0x53, 0x48, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x42, 0x24, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x0f, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x74, 0x68, 0x12, + 0x09, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x42, 0x24, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x0f, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x74, 0x68, 0x12, 0x09, 0x56, 0x61, 0x75, - 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x6c, 0x0a, 0x0b, - 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, - 0x2c, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x12, 0x0a, 0x48, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0b, 0x68, - 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x83, 0x01, 0x0a, 0x11, 0x68, - 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, - 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x42, 0x37, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x2f, 0x0a, 0x1c, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x5f, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x0f, 0x48, 0x74, - 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x52, 0x11, 0x68, - 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, - 0x42, 0x68, 0x5a, 0x66, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, - 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, - 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, 0x62, - 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, - 0x6c, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x42, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1f, 0x0a, 0x13, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x5f, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x42, 0x26, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x13, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x07, 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x6b, 0x65, 0x79, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x5f, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x62, 0x69, 0x74, 0x73, + 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x42, 0x26, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x13, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x62, + 0x69, 0x74, 0x73, 0x12, 0x07, 0x4b, 0x65, 0x79, 0x42, 0x69, 0x74, 0x73, 0x52, 0x07, 0x6b, 0x65, + 0x79, 0x42, 0x69, 0x74, 0x73, 0x12, 0x4d, 0x0a, 0x03, 0x74, 0x74, 0x6c, 0x18, 0x32, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x42, 0x1d, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x15, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x74, 0x74, 0x6c, 0x12, 0x03, 0x54, 0x74, 0x6c, 0x52, + 0x03, 0x74, 0x74, 0x6c, 0x12, 0x57, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x3c, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x42, 0x22, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x11, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x12, + 0x05, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0xd7, 0x01, + 0x0a, 0x10, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x46, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, + 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x75, 0x6c, 0x74, + 0x53, 0x53, 0x48, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x72, + 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x69, 0x74, 0x69, 0x63, + 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x36, + 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x2e, 0x0a, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x6f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x0f, 0x43, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0xbc, 0x01, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x50, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x6f, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x56, + 0x61, 0x75, 0x6c, 0x74, 0x53, 0x53, 0x48, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, + 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x45, 0x78, + 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x2b, 0xa0, + 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x23, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x0a, + 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x42, 0x0a, 0x14, 0x43, 0x72, 0x69, 0x74, 0x69, 0x63, + 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3d, 0x0a, 0x0f, 0x45, 0x78, + 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x68, 0x5a, 0x66, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, + 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x69, 0x65, 0x73, 0x3b, + 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, + 0x69, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -416,32 +657,46 @@ func file_controller_api_resources_credentiallibraries_v1_credential_library_pro return file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDescData } -var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_goTypes = []interface{}{ - (*CredentialLibrary)(nil), // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary - (*VaultCredentialLibraryAttributes)(nil), // 1: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes - (*scopes.ScopeInfo)(nil), // 2: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 3: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 5: google.protobuf.Struct + (*CredentialLibrary)(nil), // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary + (*VaultCredentialLibraryAttributes)(nil), // 1: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes + (*VaultSSHCertificateCredentialLibraryAttributes)(nil), // 2: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes + nil, // 3: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry + nil, // 4: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry + (*scopes.ScopeInfo)(nil), // 5: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 6: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 8: google.protobuf.Struct + (*wrapperspb.UInt32Value)(nil), // 9: google.protobuf.UInt32Value } var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_depIdxs = []int32{ - 2, // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 3, // 1: controller.api.resources.credentiallibraries.v1.CredentialLibrary.name:type_name -> google.protobuf.StringValue - 3, // 2: controller.api.resources.credentiallibraries.v1.CredentialLibrary.description:type_name -> google.protobuf.StringValue - 4, // 3: controller.api.resources.credentiallibraries.v1.CredentialLibrary.created_time:type_name -> google.protobuf.Timestamp - 4, // 4: controller.api.resources.credentiallibraries.v1.CredentialLibrary.updated_time:type_name -> google.protobuf.Timestamp - 5, // 5: controller.api.resources.credentiallibraries.v1.CredentialLibrary.attributes:type_name -> google.protobuf.Struct + 5, // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 6, // 1: controller.api.resources.credentiallibraries.v1.CredentialLibrary.name:type_name -> google.protobuf.StringValue + 6, // 2: controller.api.resources.credentiallibraries.v1.CredentialLibrary.description:type_name -> google.protobuf.StringValue + 7, // 3: controller.api.resources.credentiallibraries.v1.CredentialLibrary.created_time:type_name -> google.protobuf.Timestamp + 7, // 4: controller.api.resources.credentiallibraries.v1.CredentialLibrary.updated_time:type_name -> google.protobuf.Timestamp + 8, // 5: controller.api.resources.credentiallibraries.v1.CredentialLibrary.attributes:type_name -> google.protobuf.Struct 1, // 6: controller.api.resources.credentiallibraries.v1.CredentialLibrary.vault_credential_library_attributes:type_name -> controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes - 5, // 7: controller.api.resources.credentiallibraries.v1.CredentialLibrary.credential_mapping_overrides:type_name -> google.protobuf.Struct - 3, // 8: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue - 3, // 9: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_method:type_name -> google.protobuf.StringValue - 3, // 10: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_request_body:type_name -> google.protobuf.StringValue - 11, // [11:11] is the sub-list for method output_type - 11, // [11:11] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 2, // 7: controller.api.resources.credentiallibraries.v1.CredentialLibrary.vault_ssh_certificate_credential_library_attributes:type_name -> controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes + 1, // 8: controller.api.resources.credentiallibraries.v1.CredentialLibrary.vault_generic_credential_library_attributes:type_name -> controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes + 8, // 9: controller.api.resources.credentiallibraries.v1.CredentialLibrary.credential_mapping_overrides:type_name -> google.protobuf.Struct + 6, // 10: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue + 6, // 11: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_method:type_name -> google.protobuf.StringValue + 6, // 12: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_request_body:type_name -> google.protobuf.StringValue + 6, // 13: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue + 6, // 14: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.username:type_name -> google.protobuf.StringValue + 6, // 15: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_type:type_name -> google.protobuf.StringValue + 9, // 16: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_bits:type_name -> google.protobuf.UInt32Value + 6, // 17: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ttl:type_name -> google.protobuf.StringValue + 6, // 18: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_id:type_name -> google.protobuf.StringValue + 3, // 19: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.critical_options:type_name -> controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry + 4, // 20: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.extensions:type_name -> controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry + 21, // [21:21] is the sub-list for method output_type + 21, // [21:21] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_controller_api_resources_credentiallibraries_v1_credential_library_proto_init() } @@ -474,10 +729,24 @@ func file_controller_api_resources_credentiallibraries_v1_credential_library_pro return nil } } + file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VaultSSHCertificateCredentialLibraryAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes[0].OneofWrappers = []interface{}{ (*CredentialLibrary_Attributes)(nil), (*CredentialLibrary_VaultCredentialLibraryAttributes)(nil), + (*CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes)(nil), + (*CredentialLibrary_VaultGenericCredentialLibraryAttributes)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -485,7 +754,7 @@ func file_controller_api_resources_credentiallibraries_v1_credential_library_pro GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/testing/internal/e2e/tests/static_with_vault/credential_store_test.go b/testing/internal/e2e/tests/static_with_vault/credential_store_test.go index 339c7eb68..c29749193 100644 --- a/testing/internal/e2e/tests/static_with_vault/credential_store_test.go +++ b/testing/internal/e2e/tests/static_with_vault/credential_store_test.go @@ -256,7 +256,7 @@ func TestApiVaultCredentialStore(t *testing.T) { // Create a credential library for the private key clClient := credentiallibraries.NewClient(client) - newCredentialLibraryResult, err := clClient.Create(ctx, newCredentialStoreId, + newCredentialLibraryResult, err := clClient.Create(ctx, "vault", newCredentialStoreId, credentiallibraries.WithVaultCredentialLibraryPath(c.VaultSecretPath+"/data/"+privateKeySecretName), credentiallibraries.WithCredentialType("ssh_private_key"), ) @@ -265,7 +265,7 @@ func TestApiVaultCredentialStore(t *testing.T) { t.Logf("Created Credential Library: %s", newPrivateKeyCredentialLibraryId) // Create a credential library for the password - newCredentialLibraryResult, err = clClient.Create(ctx, newCredentialStoreId, + newCredentialLibraryResult, err = clClient.Create(ctx, "vault", newCredentialStoreId, credentiallibraries.WithVaultCredentialLibraryPath(c.VaultSecretPath+"/data/"+passwordSecretName), credentiallibraries.WithCredentialType("username_password"), ) From 89a55632ba51a4e2c85903c4f57dfbfbc266b30a Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Tue, 24 Jan 2023 13:01:20 +0000 Subject: [PATCH 05/11] feat(cli): Add vault ssh certificate credential library --- internal/cmd/commands.go | 26 +- .../commands/credentiallibrariescmd/funcs.go | 23 +- .../vault-generic_credentiallibraries.gen.go | 275 ++++++++++++++++++ .../vault-generic_funcs.go | 174 +++++++++++ ...ssh-certificate_credentiallibraries.gen.go | 275 ++++++++++++++++++ .../vault-ssh-certificate_funcs.go | 264 +++++++++++++++++ .../credentiallibrariescmd/vault_funcs.go | 35 +-- internal/cmd/common/flags.go | 7 +- internal/cmd/gencli/input.go | 32 ++ internal/tests/cli/Makefile | 31 ++ .../cli/boundary/_credential_libraries.bash | 36 +++ .../cli/boundary/_credential_stores.bash | 4 + internal/tests/cli/boundary/_vault.bash | 47 +++ .../cli/boundary/credential_libraries.bats | 181 ++++++++++++ 14 files changed, 1376 insertions(+), 34 deletions(-) create mode 100644 internal/cmd/commands/credentiallibrariescmd/vault-generic_credentiallibraries.gen.go create mode 100644 internal/cmd/commands/credentiallibrariescmd/vault-generic_funcs.go create mode 100644 internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_credentiallibraries.gen.go create mode 100644 internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_funcs.go create mode 100644 internal/tests/cli/boundary/_credential_libraries.bash create mode 100644 internal/tests/cli/boundary/_vault.bash create mode 100755 internal/tests/cli/boundary/credential_libraries.bats diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 27a0d473c..3b3066e4d 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -364,6 +364,18 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "credential-libraries create vault-generic": func() (cli.Command, error) { + return &credentiallibrariescmd.VaultGenericCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, + "credential-libraries create vault-ssh-certificate": func() (cli.Command, error) { + return &credentiallibrariescmd.VaultSshCertificateCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "credential-libraries update": func() (cli.Command, error) { return &credentiallibrariescmd.Command{ Command: base.NewCommand(ui), @@ -371,7 +383,19 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, nil }, "credential-libraries update vault": func() (cli.Command, error) { - return &credentiallibrariescmd.VaultCommand{ + return &credentiallibrariescmd.VaultGenericCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, + "credential-libraries update vault-generic": func() (cli.Command, error) { + return &credentiallibrariescmd.VaultGenericCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, + "credential-libraries update vault-ssh-certificate": func() (cli.Command, error) { + return &credentiallibrariescmd.VaultSshCertificateCommand{ Command: base.NewCommand(ui), Func: "update", }, nil diff --git a/internal/cmd/commands/credentiallibrariescmd/funcs.go b/internal/cmd/commands/credentiallibrariescmd/funcs.go index e6a3efa1b..b131536b9 100644 --- a/internal/cmd/commands/credentiallibrariescmd/funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/funcs.go @@ -140,6 +140,16 @@ func printItemTable(item *credentiallibraries.CredentialLibrary, resp *api.Respo nonAttributeMap["Type"] = item.Type } + var keySubstMap map[string]string + switch item.Type { + case "vault": + fallthrough + case "vault-generic": + keySubstMap = genericKeySubstMap + case "vault-ssh-certificate": + keySubstMap = sshCertKeySubstMap + } + maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) ret := []string{ @@ -189,8 +199,19 @@ func printItemTable(item *credentiallibraries.CredentialLibrary, resp *api.Respo return base.WrapForHelpText(ret) } -var keySubstMap = map[string]string{ +var genericKeySubstMap = map[string]string{ "path": "Path", "http_method": "HTTP Method", "http_request_body": "HTTP Request Body", } + +var sshCertKeySubstMap = map[string]string{ + "path": "Path", + "username": "Username", + "key_type": "Key Type", + "key_bits": "Key Bits", + "ttl": "TTL", + "key_id": "Key ID", + "critical_options": "Critical Options", + "extensions": "Extensions", +} diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-generic_credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/vault-generic_credentiallibraries.gen.go new file mode 100644 index 000000000..0aaa8b27d --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-generic_credentiallibraries.gen.go @@ -0,0 +1,275 @@ +// Code generated by "make cli"; DO NOT EDIT. +package credentiallibrariescmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initVaultGenericFlags() { + flagsOnce.Do(func() { + extraFlags := extraVaultGenericActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsVaultGenericMap[k] = append(flagsVaultGenericMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*VaultGenericCommand)(nil) + _ cli.CommandAutocomplete = (*VaultGenericCommand)(nil) +) + +type VaultGenericCommand struct { + *base.Command + + Func string + + plural string + + extraVaultGenericCmdVars +} + +func (c *VaultGenericCommand) AutocompleteArgs() complete.Predictor { + initVaultGenericFlags() + return complete.PredictAnything +} + +func (c *VaultGenericCommand) AutocompleteFlags() complete.Flags { + initVaultGenericFlags() + return c.Flags().Completions() +} + +func (c *VaultGenericCommand) Synopsis() string { + if extra := extraVaultGenericSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential library" + + synopsisStr = fmt.Sprintf("%s %s", "vault-generic-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *VaultGenericCommand) Help() string { + initVaultGenericFlags() + + var helpStr string + helpMap := common.HelpMap("credential library") + + switch c.Func { + + default: + + helpStr = c.extraVaultGenericHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsVaultGenericMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *VaultGenericCommand) Flags() *base.FlagSets { + if len(flagsVaultGenericMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "vault-generic-type credential library", flagsVaultGenericMap, c.Func) + + extraVaultGenericFlagsFunc(c, set, f) + + return set +} + +func (c *VaultGenericCommand) Run(args []string) int { + initVaultGenericFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "vault-generic-type credential library" + switch c.Func { + case "list": + c.plural = "vault-generic-type credential librarys" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsVaultGenericMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []credentiallibraries.Option + + if strutil.StrListContains(flagsVaultGenericMap[c.Func], "credential-store-id") { + switch c.Func { + + case "create": + if c.FlagCredentialStoreId == "" { + c.PrintCliError(errors.New("CredentialStore ID must be passed in via -credential-store-id or BOUNDARY_CREDENTIAL_STORE_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + credentiallibrariesClient := credentiallibraries.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultName()) + default: + opts = append(opts, credentiallibraries.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultDescription()) + default: + opts = append(opts, credentiallibraries.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, credentiallibraries.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, credentiallibraries.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraVaultGenericFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *credentiallibraries.CredentialLibrary + + var createResult *credentiallibraries.CredentialLibraryCreateResult + + var updateResult *credentiallibraries.CredentialLibraryUpdateResult + + switch c.Func { + + case "create": + createResult, err = credentiallibrariesClient.Create(c.Context, "vault-generic", c.FlagCredentialStoreId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = credentiallibrariesClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraVaultGenericActions(c, resp, item, err, credentiallibrariesClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomVaultGenericActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *VaultGenericCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + extraVaultGenericActionsFlagsMapFunc = func() map[string][]string { return nil } + extraVaultGenericSynopsisFunc = func(*VaultGenericCommand) string { return "" } + extraVaultGenericFlagsFunc = func(*VaultGenericCommand, *base.FlagSets, *base.FlagSet) {} + extraVaultGenericFlagsHandlingFunc = func(*VaultGenericCommand, *base.FlagSets, *[]credentiallibraries.Option) bool { return true } + executeExtraVaultGenericActions = func(_ *VaultGenericCommand, inResp *api.Response, inItem *credentiallibraries.CredentialLibrary, inErr error, _ *credentiallibraries.Client, _ uint32, _ []credentiallibraries.Option) (*api.Response, *credentiallibraries.CredentialLibrary, error) { + return inResp, inItem, inErr + } + printCustomVaultGenericActionOutput = func(*VaultGenericCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-generic_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault-generic_funcs.go new file mode 100644 index 000000000..b2192b02b --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-generic_funcs.go @@ -0,0 +1,174 @@ +package credentiallibrariescmd + +import ( + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/go-secure-stdlib/parseutil" +) + +func init() { + extraVaultGenericFlagsFunc = extraVaultGenericFlagsFuncImpl + extraVaultGenericActionsFlagsMapFunc = extraVaultGenericActionsFlagsMapFuncImpl + extraVaultGenericFlagsHandlingFunc = extraVaultGenericFlagHandlingFuncImpl +} + +const ( + pathFlagName = "vault-path" + httpMethodFlagName = "vault-http-method" + httpRequestBodyFlagName = "vault-http-request-body" + credentialTypeFlagName = "credential-type" + credentialMappingFlagName = "credential-mapping-override" +) + +type extraVaultGenericCmdVars struct { + flagPath string + flagHttpMethod string + flagHttpRequestBody string + flagCredentialType string + flagCredentialMapping []base.CombinedSliceFlagValue +} + +func extraVaultGenericActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + pathFlagName, + httpMethodFlagName, + httpRequestBodyFlagName, + credentialTypeFlagName, + credentialMappingFlagName, + }, + "update": { + pathFlagName, + httpMethodFlagName, + httpRequestBodyFlagName, + credentialMappingFlagName, + }, + } + return flags +} + +func extraVaultGenericFlagsFuncImpl(c *VaultGenericCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Vault Credential Library Options") + + for _, name := range flagsVaultGenericMap[c.Func] { + switch name { + case pathFlagName: + f.StringVar(&base.StringVar{ + Name: pathFlagName, + Target: &c.flagPath, + Usage: "The path in vault to request credentials from.", + }) + case httpMethodFlagName: + f.StringVar(&base.StringVar{ + Name: httpMethodFlagName, + Target: &c.flagHttpMethod, + Usage: "The http method the library should use when communicating with vault.", + }) + case httpRequestBodyFlagName: + f.StringVar(&base.StringVar{ + Name: httpRequestBodyFlagName, + Target: &c.flagHttpRequestBody, + Usage: "The http request body the library uses to communicate with vault. This can be the value itself, refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read.", + }) + case credentialTypeFlagName: + f.StringVar(&base.StringVar{ + Name: credentialTypeFlagName, + Target: &c.flagCredentialType, + Usage: "The type of credential this library will issue, defaults to Unspecified.", + }) + case credentialMappingFlagName: + f.CombinationSliceVar(&base.CombinationSliceVar{ + Name: credentialMappingFlagName, + Target: &c.flagCredentialMapping, + KvSplit: true, + Usage: "The credential mapping override.", + }) + } + } +} + +func extraVaultGenericFlagHandlingFuncImpl(c *VaultGenericCommand, _ *base.FlagSets, opts *[]credentiallibraries.Option) bool { + switch c.flagPath { + case "": + default: + *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryPath(c.flagPath)) + } + switch c.flagHttpMethod { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultCredentialLibraryHttpMethod()) + default: + *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryHttpMethod(c.flagHttpMethod)) + } + switch c.flagHttpRequestBody { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultCredentialLibraryHttpRequestBody()) + default: + rb, _ := parseutil.ParsePath(c.flagHttpRequestBody) + *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryHttpRequestBody(rb)) + } + switch c.flagCredentialType { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultCredentialType()) + default: + *opts = append(*opts, credentiallibraries.WithCredentialType(c.flagCredentialType)) + } + switch len(c.flagCredentialMapping) { + case 0: + case 1: + if len(c.flagCredentialMapping[0].Keys) == 1 && c.flagCredentialMapping[0].Keys[0] == "null" && c.flagCredentialMapping[0].Value == nil { + *opts = append(*opts, credentiallibraries.DefaultCredentialMappingOverrides()) + break + } + fallthrough + default: + mappings := make(map[string]any, len(c.flagCredentialMapping)) + for _, mapping := range c.flagCredentialMapping { + switch { + case len(mapping.Keys) != 1 || mapping.Keys[0] == "" || mapping.Value == nil || mapping.Value.GetValue() == "": + // mapping override does not support key segments (e.g. 'x.y=z') + c.UI.Error("Credential mapping override must be in the format 'key=value', 'key=null' to clear field or 'null' to clear all.") + return false + case mapping.Value.GetValue() == "null": + // user provided 'key=null' indicating the field specific override should + // be cleared, set map value to nil + mappings[mapping.Keys[0]] = nil + default: + mappings[mapping.Keys[0]] = mapping.Value.GetValue() + } + } + *opts = append(*opts, credentiallibraries.WithCredentialMappingOverrides(mappings)) + } + + return true +} + +func (c *VaultGenericCommand) extraVaultGenericHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries create vault -credential-store-id [options] [args]", + "", + " Create a vault-generic-type credential library. Example:", + "", + ` $ boundary credential-libraries create vault-generic -credential-store-id csvlt_1234567890 -vault-path "/some/path"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries update vault [options] [args]", + "", + " Update a vault-generic-type credential library given its ID. Example:", + "", + ` $ boundary credential-libraries update vault-generic -id clvlt_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_credentiallibraries.gen.go new file mode 100644 index 000000000..bdc891939 --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_credentiallibraries.gen.go @@ -0,0 +1,275 @@ +// Code generated by "make cli"; DO NOT EDIT. +package credentiallibrariescmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initVaultSshCertificateFlags() { + flagsOnce.Do(func() { + extraFlags := extraVaultSshCertificateActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsVaultSshCertificateMap[k] = append(flagsVaultSshCertificateMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*VaultSshCertificateCommand)(nil) + _ cli.CommandAutocomplete = (*VaultSshCertificateCommand)(nil) +) + +type VaultSshCertificateCommand struct { + *base.Command + + Func string + + plural string + + extraVaultSshCertificateCmdVars +} + +func (c *VaultSshCertificateCommand) AutocompleteArgs() complete.Predictor { + initVaultSshCertificateFlags() + return complete.PredictAnything +} + +func (c *VaultSshCertificateCommand) AutocompleteFlags() complete.Flags { + initVaultSshCertificateFlags() + return c.Flags().Completions() +} + +func (c *VaultSshCertificateCommand) Synopsis() string { + if extra := extraVaultSshCertificateSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential library" + + synopsisStr = fmt.Sprintf("%s %s", "vault-ssh-certificate-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *VaultSshCertificateCommand) Help() string { + initVaultSshCertificateFlags() + + var helpStr string + helpMap := common.HelpMap("credential library") + + switch c.Func { + + default: + + helpStr = c.extraVaultSshCertificateHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsVaultSshCertificateMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *VaultSshCertificateCommand) Flags() *base.FlagSets { + if len(flagsVaultSshCertificateMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "vault-ssh-certificate-type credential library", flagsVaultSshCertificateMap, c.Func) + + extraVaultSshCertificateFlagsFunc(c, set, f) + + return set +} + +func (c *VaultSshCertificateCommand) Run(args []string) int { + initVaultSshCertificateFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "vault-ssh-certificate-type credential library" + switch c.Func { + case "list": + c.plural = "vault-ssh-certificate-type credential librarys" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsVaultSshCertificateMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []credentiallibraries.Option + + if strutil.StrListContains(flagsVaultSshCertificateMap[c.Func], "credential-store-id") { + switch c.Func { + + case "create": + if c.FlagCredentialStoreId == "" { + c.PrintCliError(errors.New("CredentialStore ID must be passed in via -credential-store-id or BOUNDARY_CREDENTIAL_STORE_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + credentiallibrariesClient := credentiallibraries.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultName()) + default: + opts = append(opts, credentiallibraries.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultDescription()) + default: + opts = append(opts, credentiallibraries.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, credentiallibraries.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, credentiallibraries.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraVaultSshCertificateFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *credentiallibraries.CredentialLibrary + + var createResult *credentiallibraries.CredentialLibraryCreateResult + + var updateResult *credentiallibraries.CredentialLibraryUpdateResult + + switch c.Func { + + case "create": + createResult, err = credentiallibrariesClient.Create(c.Context, "vault-ssh-certificate", c.FlagCredentialStoreId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = credentiallibrariesClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraVaultSshCertificateActions(c, resp, item, err, credentiallibrariesClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomVaultSshCertificateActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *VaultSshCertificateCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + extraVaultSshCertificateActionsFlagsMapFunc = func() map[string][]string { return nil } + extraVaultSshCertificateSynopsisFunc = func(*VaultSshCertificateCommand) string { return "" } + extraVaultSshCertificateFlagsFunc = func(*VaultSshCertificateCommand, *base.FlagSets, *base.FlagSet) {} + extraVaultSshCertificateFlagsHandlingFunc = func(*VaultSshCertificateCommand, *base.FlagSets, *[]credentiallibraries.Option) bool { return true } + executeExtraVaultSshCertificateActions = func(_ *VaultSshCertificateCommand, inResp *api.Response, inItem *credentiallibraries.CredentialLibrary, inErr error, _ *credentiallibraries.Client, _ uint32, _ []credentiallibraries.Option) (*api.Response, *credentiallibraries.CredentialLibrary, error) { + return inResp, inItem, inErr + } + printCustomVaultSshCertificateActionOutput = func(*VaultSshCertificateCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_funcs.go new file mode 100644 index 000000000..23d588cab --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_funcs.go @@ -0,0 +1,264 @@ +package credentiallibrariescmd + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" +) + +func init() { + extraVaultSshCertificateFlagsFunc = extraVaultSshCertificateFlagsFuncImpl + extraVaultSshCertificateActionsFlagsMapFunc = extraVaultSshCertificateActionsFlagsMapFuncImpl + extraVaultSshCertificateFlagsHandlingFunc = extraVaultSshCertificateFlagHandlingFuncImpl +} + +const ( + usernameName = "username" + keyTypeName = "key-type" + keyBitsName = "key-bits" + ttlName = "ttl" + keyIdName = "key-id" + criticalOptionsName = "critical-options" + piecewiseCriticalOptionsName = "critical-option" + extensionsName = "extensions" + piecewiseExtensionName = "extension" +) + +type extraVaultSshCertificateCmdVars struct { + flagPath string + flagUsername string + flagKeyType string + flagKeyBits string + flagTtl string + flagKeyId string + flagCriticalOptions string + flagCriticalOpts []base.CombinedSliceFlagValue + flagExtensions string + flagExtens []base.CombinedSliceFlagValue +} + +func extraVaultSshCertificateActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + pathFlagName, + usernameName, + keyTypeName, + keyBitsName, + ttlName, + keyIdName, + criticalOptionsName, + piecewiseCriticalOptionsName, + extensionsName, + piecewiseExtensionName, + }, + "update": { + pathFlagName, + usernameName, + keyTypeName, + keyBitsName, + ttlName, + keyIdName, + criticalOptionsName, + piecewiseCriticalOptionsName, + extensionsName, + piecewiseExtensionName, + }, + } + return flags +} + +func extraVaultSshCertificateFlagsFuncImpl(c *VaultSshCertificateCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Vault SSH Certificate Credential Library Options") + + for _, name := range flagsVaultSshCertificateMap[c.Func] { + switch name { + case pathFlagName: + f.StringVar(&base.StringVar{ + Name: pathFlagName, + Target: &c.flagPath, + Usage: "The path in vault to request credentials from.", + }) + case usernameName: + f.StringVar(&base.StringVar{ + Name: usernameName, + Target: &c.flagUsername, + Usage: "The username to use with the ssh certificate.", + }) + case keyTypeName: + f.StringVar(&base.StringVar{ + Name: keyTypeName, + Target: &c.flagKeyType, + Usage: "The key type for the generated ssh private key. One of: ed25519, ecdsa, rsa.", + }) + case keyBitsName: + f.StringVar(&base.StringVar{ + Name: keyBitsName, + Target: &c.flagKeyBits, + Usage: "The number of bits when generating the ssh private key. Depends on key_type. If ed25519 this should not be set, or set to 0, if ecdsa one of 256, 384, 521, if rsa one of 2048, 3072, 4096.", + }) + case ttlName: + f.StringVar(&base.StringVar{ + Name: ttlName, + Target: &c.flagTtl, + Usage: "The time-to-live for the generated certificate.", + }) + case keyIdName: + f.StringVar(&base.StringVar{ + Name: keyIdName, + Target: &c.flagKeyId, + Usage: "The key id that the created certificate should have.", + }) + } + } + criticalOptsInput := common.CombinedSliceFlagValuePopulationInput{ + FlagSet: f, + FlagNames: flagsVaultSshCertificateMap[c.Func], + FullPopulationFlag: &c.flagCriticalOptions, + FullPopulationInputName: criticalOptionsName, + PiecewisePopulationFlag: &c.flagCriticalOpts, + PiecewisePopulationInputBaseName: piecewiseCriticalOptionsName, + PiecewiseNoProtoCompat: true, + } + common.PopulateCombinedSliceFlagValue(criticalOptsInput) + + extensionsInput := common.CombinedSliceFlagValuePopulationInput{ + FlagSet: f, + FlagNames: flagsVaultSshCertificateMap[c.Func], + FullPopulationFlag: &c.flagExtensions, + FullPopulationInputName: extensionsName, + PiecewisePopulationFlag: &c.flagExtens, + PiecewisePopulationInputBaseName: piecewiseExtensionName, + PiecewiseNoProtoCompat: true, + } + common.PopulateCombinedSliceFlagValue(extensionsInput) +} + +func extraVaultSshCertificateFlagHandlingFuncImpl(c *VaultSshCertificateCommand, _ *base.FlagSets, opts *[]credentiallibraries.Option) bool { + switch c.flagPath { + case "": + default: + *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryPath(c.flagPath)) + } + switch c.flagUsername { + case "": + default: + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryUsername(c.flagUsername)) + } + switch c.flagKeyType { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryKeyType()) + default: + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyType(c.flagKeyType)) + } + switch c.flagKeyBits { + case "": + case "0", "null": + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryKeyBits()) + default: + var final uint32 + keyBits, err := strconv.ParseUint(c.flagKeyBits, 10, 32) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing %q: %s", c.flagKeyBits, err)) + return false + } + final = uint32(keyBits) + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyBits(final)) + } + switch c.flagTtl { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryTtl()) + default: + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryTtl(c.flagTtl)) + } + switch c.flagKeyId { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryKeyId()) + default: + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyId(c.flagKeyId)) + } + + if err := common.HandleAttributeFlags( + c.Command, + piecewiseCriticalOptionsName, + c.flagCriticalOptions, + c.flagCriticalOpts, + func() { + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryCriticalOptions()) + }, + func(in map[string]any) { + inn := make(map[string]string, len(in)) + for k, v := range in { + switch vv := v.(type) { + case nil: + inn[k] = "" + case string: + inn[k] = vv + default: + continue + } + } + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryCriticalOptions(inn)) + }); err != nil { + return false + } + if err := common.HandleAttributeFlags( + c.Command, + piecewiseExtensionName, + c.flagExtensions, + c.flagExtens, + func() { + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryExtensions()) + }, + func(in map[string]any) { + inn := make(map[string]string, len(in)) + for k, v := range in { + switch vv := v.(type) { + case nil: + inn[k] = "" + case string: + inn[k] = vv + default: + continue + } + } + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryExtensions(inn)) + }); err != nil { + return false + } + + return true +} + +func (c *VaultSshCertificateCommand) extraVaultSshCertificateHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries create vault-ssh-certificate -credential-store-id [options] [args]", + "", + " Create a vault-ssh-certificate-type credential library. Example:", + "", + ` $ boundary credential-libraries create vault-ssh-certificate -credential-store-id csvlt_1234567890 -vault-path "/ssh/sign/role" -username user`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries update vault-ssh-certificate [options] [args]", + "", + " Update a vault-ssh-certificate-type credential library given its ID. Example:", + "", + ` $ boundary credential-libraries update vault-ssh-certificate -id clvsclt_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go index d066a2cde..867e0e85c 100644 --- a/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go @@ -12,39 +12,10 @@ func init() { extraVaultFlagsHandlingFunc = extraVaultFlagHandlingFuncImpl } -const ( - pathFlagName = "vault-path" - httpMethodFlagName = "vault-http-method" - httpRequestBodyFlagName = "vault-http-request-body" - credentialTypeFlagName = "credential-type" - credentialMappingFlagName = "credential-mapping-override" -) - -type extraVaultCmdVars struct { - flagPath string - flagHttpMethod string - flagHttpRequestBody string - flagCredentialType string - flagCredentialMapping []base.CombinedSliceFlagValue -} +type extraVaultCmdVars extraVaultGenericCmdVars func extraVaultActionsFlagsMapFuncImpl() map[string][]string { - flags := map[string][]string{ - "create": { - pathFlagName, - httpMethodFlagName, - httpRequestBodyFlagName, - credentialTypeFlagName, - credentialMappingFlagName, - }, - "update": { - pathFlagName, - httpMethodFlagName, - httpRequestBodyFlagName, - credentialMappingFlagName, - }, - } - return flags + return extraVaultGenericActionsFlagsMapFuncImpl() } func extraVaultFlagsFuncImpl(c *VaultCommand, set *base.FlagSets, _ *base.FlagSet) { @@ -150,6 +121,7 @@ func (c *VaultCommand) extraVaultHelpFunc(_ map[string]func() string) string { switch c.Func { case "create": helpStr = base.WrapForHelpText([]string{ + "Deprecated: use 'boundary credential-libraries create vault-generic' instead.", "Usage: boundary credential-libraries create vault -credential-store-id [options] [args]", "", " Create a vault-type credential library. Example:", @@ -161,6 +133,7 @@ func (c *VaultCommand) extraVaultHelpFunc(_ map[string]func() string) string { case "update": helpStr = base.WrapForHelpText([]string{ + "Deprecated: use 'boundary credential-libraries update vault-generic' instead.", "Usage: boundary credential-libraries update vault [options] [args]", "", " Update a vault-type credential library given its ID. Example:", diff --git a/internal/cmd/common/flags.go b/internal/cmd/common/flags.go index f907545ed..07d6ead52 100644 --- a/internal/cmd/common/flags.go +++ b/internal/cmd/common/flags.go @@ -139,6 +139,11 @@ type CombinedSliceFlagValuePopulationInput struct { // combination method, e.g. "attr" will be used to build "string-attr"; also // used for generating help texts PiecewisePopulationInputBaseName string + + // If ProtoCompat is true, the key will be validated against proto3 syntax + // requirements for identifiers. If the string is split via KeyDelimiter, each + // segment will be evaluated independently. + PiecewiseNoProtoCompat bool } func PopulateCombinedSliceFlagValue(input CombinedSliceFlagValuePopulationInput) { @@ -163,7 +168,7 @@ func PopulateCombinedSliceFlagValue(input CombinedSliceFlagValuePopulationInput) Target: input.PiecewisePopulationFlag, KvSplit: true, KeyDelimiter: &keyDelimiter, - ProtoCompatKey: true, + ProtoCompatKey: !input.PiecewiseNoProtoCompat, KeyOnlyAllowed: true, Usage: fmt.Sprintf( "A key=value pair to add to the request's %s map. "+ diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 20b6b2b61..a7df2d470 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -237,9 +237,41 @@ var inputStructs = map[string][]*cmdInfo{ HasId: true, HasName: true, HasDescription: true, + NeedsSubtypeInCreate: true, Container: "CredentialStore", VersionedActions: []string{"update"}, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + }, + { + ResourceType: resource.CredentialLibrary.String(), + Pkg: "credentiallibraries", + StdActions: []string{"create", "update"}, + SubActionPrefix: "vault-generic", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, NeedsSubtypeInCreate: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + }, + { + ResourceType: resource.CredentialLibrary.String(), + Pkg: "credentiallibraries", + StdActions: []string{"create", "update"}, + SubActionPrefix: "vault-ssh-certificate", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + NeedsSubtypeInCreate: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, PrefixAttributeFieldErrorsWithSubactionPrefix: true, }, }, diff --git a/internal/tests/cli/Makefile b/internal/tests/cli/Makefile index f153e4e37..b9531bd03 100644 --- a/internal/tests/cli/Makefile +++ b/internal/tests/cli/Makefile @@ -1,5 +1,36 @@ all: test +REGISTRY_NAME ?= docker.io/hashicorp +TEST_VAULT_IMAGE_NAME=vault +TEST_VAULT_IMAGE ?= $(REGISTRY_NAME)/$(TEST_VAULT_IMAGE_NAME):1.12.2 +TEST_VAULT_PORT ?= 8200 +TEST_VAULT_CONTAINER_NAME ?= boundary-cli-tests-vault + +VAULT_ADDR ?= "http://127.0.0.1:$(TEST_VAULT_PORT)" +VAULT_TOKEN ?= boundarytok + +.PHONY: test-vault-up +test-vault-up: + @echo "Using image: $(TEST_VAULT_IMAGE)" + @echo " vault addr: $(VAULT_ADDR)" + @echo "vault token: $(VAULT_TOKEN)" + @docker run \ + -d \ + --name $(TEST_VAULT_CONTAINER_NAME) \ + -e VAULT_DEV_ROOT_TOKEN_ID=$(VAULT_TOKEN) \ + -p $(TEST_VAULT_PORT):8200 \ + --cap-add IPC_LOCK \ + $(TEST_VAULT_IMAGE) + @echo "Container name: $(TEST_VAULT_CONTAINER_NAME)" + @echo "Test vault available at: $(VAULT_ADDR)" + @echo "For vault logs run:" + @echo " docker logs $(TEST_VAULT_CONTAINER_NAME)" + +.PHONY: test-vault-down +test-vault-down: + docker stop $(TEST_VAULT_CONTAINER_NAME) || true + docker rm -v $(TEST_VAULT_CONTAINER_NAME) || true + .PHONY: test test: ./test.sh diff --git a/internal/tests/cli/boundary/_credential_libraries.bash b/internal/tests/cli/boundary/_credential_libraries.bash new file mode 100644 index 000000000..606399e6b --- /dev/null +++ b/internal/tests/cli/boundary/_credential_libraries.bash @@ -0,0 +1,36 @@ +load _authorized_actions + +function create_vault_ssh_certificate_library() { + boundary credential-libraries \ + create vault-ssh-certificate $@ +} + +function create_vault_generic_library() { + boundary credential-libraries \ + create vault-generic $@ +} + +function create_vault_library() { + boundary credential-libraries \ + create vault $@ +} + +function read_credential_library() { + boundary credential-libraries read -id $1 -format json +} + +function delete_credential_library() { + boundary credential-libraries delete -id $1 +} + +function list_credential_libraries() { + local csid=$1 + boundary credential-libraries list -credential-store-id $1 -format json +} + +function credential_library_id() { + local name=$1 + local csid=$2 + + strip $(list_credential_libraries $csid | jq -c ".items[] | select(.name | contains(\"$name\")) | .[\"id\"]") +} diff --git a/internal/tests/cli/boundary/_credential_stores.bash b/internal/tests/cli/boundary/_credential_stores.bash index 54db7cc93..26a8fb907 100644 --- a/internal/tests/cli/boundary/_credential_stores.bash +++ b/internal/tests/cli/boundary/_credential_stores.bash @@ -10,6 +10,10 @@ function create_static_credential_store() { -scope-id $sid } +function create_vault_credential_store() { + boundary credential-stores create vault $@ +} + function read_credential_store() { boundary credential-stores read -id $1 -format json } diff --git a/internal/tests/cli/boundary/_vault.bash b/internal/tests/cli/boundary/_vault.bash new file mode 100644 index 000000000..4b7955ae2 --- /dev/null +++ b/internal/tests/cli/boundary/_vault.bash @@ -0,0 +1,47 @@ + +function vault_write_boundary_policy() { + ( + cat < Date: Fri, 27 Jan 2023 22:14:01 +0000 Subject: [PATCH 06/11] ci: Start vault for cli ui tests --- .github/workflows/build.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31db2febe..2fc056e76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: echo "::set-output name=product-minor-version::$MINOR_VERSION" verify-product-metadata: - needs: + needs: - set-product-version - product-metadata runs-on: ubuntu-latest @@ -422,7 +422,7 @@ jobs: uses: actions/cache@v3 with: path: /tmp/bats-cli-ui-deps - key: enos-bats-cli-ui-deps-jq-1.6-password-store-1.7.4 + key: enos-bats-cli-ui-deps-jq-1.6-password-store-1.7.4-vault-1.12.2 - name: Set up Node for Bats install uses: actions/setup-node@v3 with: @@ -453,6 +453,13 @@ jobs: cd /tmp/bats-cli-ui-deps/pass/password-store-1.7.4 sudo make install pass init ${{ secrets.ENOS_GPG_UID }} + - name: Download Vault AMD64 binary for integration testing + if: steps.dep-cache.outputs.cache-hit != 'true' + run: | + wget https://releases.hashicorp.com/vault/1.12.2/vault_1.12.2_linux_amd64.zip -O /tmp/bats-cli-ui-deps/vault.zip + - name: Install Vault + run: | + unzip /tmp/bats-cli-ui-deps/vault.zip -d /usr/local/bin - name: Download Linux AMD64 Boundary bundle id: download uses: actions/download-artifact@v3 @@ -479,6 +486,16 @@ jobs: bash --version echo "boundary version:" boundary version + echo "vault version:" + vault version - name: Run cli bats tests run: | + export VAULT_ADDR=http://127.0.0.1:8200 + export VAULT_TOKEN=boundarytok + make -C internal/tests/cli test-vault-up + until vault status; do docker container inspect boundary-cli-tests-vault &> /dev/null || exit -1; sleep 1; done make test-cli + - name: Cleanup + if: success() || failure() + run: | + make -C internal/tests/cli test-vault-down From 6a41bf12372bf426964c1fc9747817bff2566f19 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Wed, 18 Jan 2023 20:01:14 +0000 Subject: [PATCH 07/11] feat(wh): Support vault ssh certificate credentials This expands the wh_credential_dimension to include fields for the new vault ssh certificate credential library. --- .../16/02_wh_credential_dimension.up.sql | 1 + .../16/03_wh_credential_dimension.up.sql | 1 + .../oss/postgres/44/03_targets.up.sql | 1 + .../postgres/63/03_wh_ssh_cert_library.up.sql | 254 ++++++++++++++++++ .../sqltest/initdb.d/03_widgets_persona.sql | 29 +- .../wh/credential_dimension/dimension.sql | 52 ++-- .../ssh_certificate_cred_library_ecdsa.sql | 56 ++++ .../ssh_certificate_cred_library_ed25519.sql | 56 ++++ .../ssh_certificate_cred_library_rsa.sql | 56 ++++ .../wh/credential_dimension_views/source.sql | 57 ++-- .../wh/credential_dimension_views/target.sql | 37 +-- 11 files changed, 525 insertions(+), 75 deletions(-) create mode 100644 internal/db/schema/migrations/oss/postgres/63/03_wh_ssh_cert_library.up.sql create mode 100644 internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ecdsa.sql create mode 100644 internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ed25519.sql create mode 100644 internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_rsa.sql diff --git a/internal/db/schema/migrations/oss/postgres/16/02_wh_credential_dimension.up.sql b/internal/db/schema/migrations/oss/postgres/16/02_wh_credential_dimension.up.sql index 0d2838fcc..554572213 100644 --- a/internal/db/schema/migrations/oss/postgres/16/02_wh_credential_dimension.up.sql +++ b/internal/db/schema/migrations/oss/postgres/16/02_wh_credential_dimension.up.sql @@ -65,6 +65,7 @@ begin; and o.public_id = p.parent_id and o.type = 'org'; + -- Replaced in 63/03_wh_ssh_cert_library.up.sql create view whx_credential_dimension_target as select key, credential_purpose, diff --git a/internal/db/schema/migrations/oss/postgres/16/03_wh_credential_dimension.up.sql b/internal/db/schema/migrations/oss/postgres/16/03_wh_credential_dimension.up.sql index 24bedceaf..3c846b06e 100644 --- a/internal/db/schema/migrations/oss/postgres/16/03_wh_credential_dimension.up.sql +++ b/internal/db/schema/migrations/oss/postgres/16/03_wh_credential_dimension.up.sql @@ -4,6 +4,7 @@ begin; -- with the current values in the operational tables for the given parameters. IF the values -- between operational tables and the wh_credential_dimension differ, a new row is inserted in -- the wh_credential_dimension to match the current values in the operational tables. + -- Replaced in 63/03_wh_ssh_cert_library.up.sql create function wh_upsert_credential_dimension(p_session_id wt_public_id, p_library_id wt_public_id, p_credential_purpose wh_dim_text) returns wh_dim_key as $$ declare diff --git a/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql b/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql index 59c323a53..c219a52da 100644 --- a/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql +++ b/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql @@ -157,6 +157,7 @@ begin; -- The whx_credential_dimension_source view shows the current values in the -- operational tables of the credential dimension. -- Replaces whx_credential_dimension_source defined in 16/02_wh_credential_dimension.up.sql + -- Replaced in 63/03_wh_ssh_cert_library.up.sql drop view whx_credential_dimension_source; create view whx_credential_dimension_source as select -- id is the first column in the target view diff --git a/internal/db/schema/migrations/oss/postgres/63/03_wh_ssh_cert_library.up.sql b/internal/db/schema/migrations/oss/postgres/63/03_wh_ssh_cert_library.up.sql new file mode 100644 index 000000000..f391cea0c --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/63/03_wh_ssh_cert_library.up.sql @@ -0,0 +1,254 @@ +begin; + alter table wh_credential_dimension + add column credential_library_username text, + add column credential_library_key_type_and_bits text + ; + + update wh_credential_dimension set + credential_library_username = 'Not Applicable', + credential_library_key_type_and_bits = 'Not Applicable'; + + update wh_credential_dimension set + credential_library_vault_http_request_body = 'Not Applicable' + where + credential_library_vault_http_method = 'GET' + and + credential_library_vault_http_request_body = 'None'; + + + alter table wh_credential_dimension + alter column credential_library_username type wh_dim_text, + alter column credential_library_key_type_and_bits type wh_dim_text + ; + + update wh_credential_dimension set + credential_library_type = 'vault generic credential library' + where + credential_library_type = 'vault credential library'; + + -- The whx_credential_dimension_source view shows the current values in the + -- operational tables of the credential dimension. + -- Replaces whx_credential_dimension_source defined in 44/03_targets.up.sql + drop view whx_credential_dimension_source; + create view whx_credential_dimension_source as + with vault_generic_library as ( + select vcl.public_id as public_id, + 'vault generic credential library' as type, + coalesce(vcl.name, 'None') as name, + coalesce(vcl.description, 'None') as description, + vcl.vault_path as vault_path, + vcl.http_method as http_method, + case + when vcl.http_method = 'GET' then 'Not Applicable' + else coalesce(vcl.http_request_body::text, 'None') + end as http_request_body, + 'Not Applicable' as username, + 'Not Applicable' as key_type_and_bits + from credential_vault_library as vcl + ), + vault_ssh_cert_library as ( + select vsccl.public_id as public_id, + 'vault ssh certificate credential library' as type, + coalesce(vsccl.name, 'None') as name, + coalesce(vsccl.description, 'None') as description, + vsccl.vault_path as vault_path, + 'Not Applicable' as http_method, + 'Not Applicable' as http_request_body, + vsccl.username as username, + case + when vsccl.key_type = 'ed25519' then vsccl.key_type + else vsccl.key_type || '-' || vsccl.key_bits::text + end as key_type_and_bits + from credential_vault_ssh_cert_library as vsccl + ), + final as ( + select s.public_id as session_id, + scd.credential_purpose as credential_purpose, + cl.public_id as credential_library_id, + coalesce(vcl.type, vsccl.type) as credential_library_type, + coalesce(vcl.name, vsccl.name) as credential_library_name, + coalesce(vcl.description, vsccl.description) as credential_library_description, + coalesce(vcl.vault_path, vsccl.vault_path) as credential_library_vault_path, + coalesce(vcl.http_method, vsccl.http_method) as credential_library_vault_http_method, + coalesce(vcl.http_request_body, vsccl.http_request_body) as credential_library_vault_http_request_body, + coalesce(vcl.username, vsccl.username) as credential_library_username, + coalesce(vcl.key_type_and_bits, vsccl.key_type_and_bits) as credential_library_key_type_and_bits, + cs.public_id as credential_store_id, + case + when vcs is null then 'None' + else 'vault credential store' + end as credential_store_type, + coalesce(vcs.name, 'None') as credential_store_name, + coalesce(vcs.description, 'None') as credential_store_description, + coalesce(vcs.namespace, 'None') as credential_store_vault_namespace, + coalesce(vcs.vault_address, 'None') as credential_store_vault_address, + t.public_id as target_id, + 'tcp target' as target_type, + coalesce(tt.name, 'None') as target_name, + coalesce(tt.description, 'None') as target_description, + coalesce(tt.default_port, 0) as target_default_port_number, + tt.session_max_seconds as target_session_max_seconds, + tt.session_connection_limit as target_session_connection_limit, + p.public_id as project_id, + coalesce(p.name, 'None') as project_name, + coalesce(p.description, 'None') as project_description, + o.public_id as organization_id, + coalesce(o.name, 'None') as organization_name, + coalesce(o.description, 'None') as organization_description + from session_credential_dynamic as scd + join session as s on scd.session_id = s.public_id + join credential_library as cl on scd.library_id = cl.public_id + join credential_store as cs on cl.store_id = cs.public_id + join target as t on s.target_id = t.public_id + join iam_scope as p on p.public_id = t.project_id and p.type = 'project' + join iam_scope as o on p.parent_id = o.public_id and o.type = 'org' + left join vault_generic_library as vcl on cl.public_id = vcl.public_id + left join vault_ssh_cert_library as vsccl on cl.public_id = vsccl.public_id + left join credential_vault_store as vcs on cs.public_id = vcs.public_id + left join target_tcp as tt on t.public_id = tt.public_id + ) + select session_id, + credential_purpose, + credential_library_id, + credential_library_type, + credential_library_name, + credential_library_description, + credential_library_vault_path, + credential_library_vault_http_method, + credential_library_vault_http_request_body, + credential_library_username, + credential_library_key_type_and_bits, + credential_store_id, + credential_store_type, + credential_store_name, + credential_store_description, + credential_store_vault_namespace, + credential_store_vault_address, + target_id, + target_type, + target_name, + target_description, + target_default_port_number, + target_session_max_seconds, + target_session_connection_limit, + project_id, + project_name, + project_description, + organization_id, + organization_name, + organization_description + from final; + + -- Replaces view in 16/02_wh_credential_dimension.up.sql + drop view whx_credential_dimension_target; + create view whx_credential_dimension_target as + select key, + credential_purpose, + credential_library_id, + credential_library_type, + credential_library_name, + credential_library_description, + credential_library_vault_path, + credential_library_vault_http_method, + credential_library_vault_http_request_body, + credential_library_username, + credential_library_key_type_and_bits, + credential_store_id, + credential_store_type, + credential_store_name, + credential_store_description, + credential_store_vault_namespace, + credential_store_vault_address, + target_id, + target_type, + target_name, + target_description, + target_default_port_number, + target_session_max_seconds, + target_session_connection_limit, + project_id, + project_name, + project_description, + organization_id, + organization_name, + organization_description + from wh_credential_dimension + where current_row_indicator = 'Current' + ; + + -- Replaces function in 16/03_wh_credential_dimension.up.sql + drop function wh_upsert_credential_dimension; + create function wh_upsert_credential_dimension(p_session_id wt_public_id, p_library_id wt_public_id, p_credential_purpose wh_dim_text) returns wh_dim_key + as $$ + declare + src whx_credential_dimension_target%rowtype; + target whx_credential_dimension_target%rowtype; + new_row wh_credential_dimension%rowtype; + t_id wt_public_id; + begin + select s.target_id into strict t_id + from session as s + where s.public_id = p_session_id; + + select * into target + from whx_credential_dimension_target as t + where t.credential_library_id = p_library_id + and t.target_id = t_id + and t.credential_purpose = p_credential_purpose; + + select + target.key, t.credential_purpose, + t.credential_library_id, t.credential_library_type, t.credential_library_name, t.credential_library_description, t.credential_library_vault_path, t.credential_library_vault_http_method, t.credential_library_vault_http_request_body, + t.credential_library_username, t.credential_library_key_type_and_bits, + t.credential_store_id, t.credential_store_type, t.credential_store_name, t.credential_store_description, t.credential_store_vault_namespace, t.credential_store_vault_address, + t.target_id, t.target_type, t.target_name, t.target_description, t.target_default_port_number, t.target_session_max_seconds, t.target_session_connection_limit, + t.project_id, t.project_name, t.project_description, + t.organization_id, t.organization_name, t.organization_description + into src + from whx_credential_dimension_source as t + where t.credential_library_id = p_library_id + and t.session_id = p_session_id + and t.target_id = t_id + and t.credential_purpose = p_credential_purpose; + + if src is distinct from target then + update wh_credential_dimension + set current_row_indicator = 'Expired', + row_expiration_time = current_timestamp + where credential_library_id = p_library_id + and target_id = t_id + and credential_purpose = p_credential_purpose + and current_row_indicator = 'Current'; + + insert into wh_credential_dimension ( + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + select credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + 'Current', current_timestamp, 'infinity'::timestamptz + from whx_credential_dimension_source + where credential_library_id = p_library_id + and session_id = p_session_id + and target_id = t_id + and credential_purpose = p_credential_purpose + returning * into new_row; + + return new_row.key; + end if; + + return target.key; + end + $$ language plpgsql; +commit; diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 9397376fd..b180af6ff 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -315,7 +315,9 @@ begin; (project_id, public_id, name) values ('p____bwidget', 't_________wb', 'Big Widget Target'), - ('p____swidget', 't_________ws', 'Small Widget Target'); + ('p____swidget', 't_________ws', 'Small Widget Target'), + ('p____swidget', 't________ws2', 'Small Widget Target 2'), + ('p____swidget', 't________ws3', 'Small Widget Target 3'); insert into target_host_set (project_id, target_id, host_set_id) @@ -327,7 +329,9 @@ begin; ('p____bwidget', 't_________wb', 's___1wb-plghs'), ('p____bwidget', 't_________wb', 's___2wb-plghs'), ('p____swidget', 't_________ws', 's___1ws-plghs'), - ('p____swidget', 't_________ws', 's___2ws-plghs'); + ('p____swidget', 't_________ws', 's___2ws-plghs'), + ('p____swidget', 't________ws2', 's___1ws-sths'), + ('p____swidget', 't________ws3', 's___1ws-sths'); end; $$ language plpgsql; @@ -336,9 +340,10 @@ begin; as $$ begin 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____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default'); + ('p____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default'), + ('p____swidget', 'vs______swvs', 'small widget vault store', 'None', 'https://small.vault.widget', 'default'); insert into credential_vault_token (store_id, key_id, status, token_hmac, token, last_renewal_time, expiration_time) @@ -401,13 +406,23 @@ begin; values ('vl______wvl12', 'my_username', 'my_private_key', 'my_passphrase'); + insert into credential_vault_ssh_cert_library + (store_id, public_id, name, description, vault_path, username, key_type, key_bits) + values + ('vs______swvs', 'vscl____wvl1', 'widget ssh admin cert', 'None', '/ssh/issue/admin', 'admin', 'rsa', 4096), + ('vs______swvs', 'vscl____wvl2', 'widget ssh ecdsa', 'None', '/ssh/sign/user', 'user', 'ecdsa', 521), + ('vs______swvs', 'vscl____wvl3', 'widget ssh ed25519', 'None', '/ssh/sign/user', 'user', 'ed25519', 0); + insert into target_credential_library (project_id, target_id, credential_library_id, credential_purpose) values ('p____bwidget', 't_________wb', 'vl______wvl1', 'brokered'), ('p____bwidget', 't_________wb', 'vl______wvl2', 'brokered'), ('p____bwidget', 't_________wb', 'vl______wvl3', 'brokered'), - ('p____bwidget', 't_________wb', 'vl______wvl3', 'injected_application'); + ('p____bwidget', 't_________wb', 'vl______wvl3', 'injected_application'), + ('p____bwidget', 't_________ws', 'vscl____wvl1', 'injected_application'), + ('p____bwidget', 't________ws2', 'vscl____wvl2', 'injected_application'), + ('p____bwidget', 't________ws3', 'vscl____wvl3', 'injected_application'); insert into credential_static_store (project_id, public_id, name, description) @@ -432,9 +447,9 @@ create function _wtt_load_widgets_sessions() returns void as $$ begin insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + (project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____warren' , 'tok___warren' , 'abc'::bytea , 'ep1' , 's1____warren'); + ('p____swidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____warren', 'tok___warren', 'abc'::bytea, 'ep1', 's1____warren'); end; $$ language plpgsql; diff --git a/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql b/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql index 7a9326b70..bc9b1644e 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql @@ -3,20 +3,22 @@ begin; insert into wh_credential_dimension ( credential_purpose, - credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, - credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, - target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, - project_id, project_name, project_description, - organization_id, organization_name, organization_description, - current_row_indicator, row_effective_time, row_expiration_time + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time ) values ( 'brokered', - 'vl______wvl1', 'vault credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', - 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', - 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, - 'p____bwidget', 'Big Widget Factory', 'None', - 'o_____widget', 'Widget Inc', 'None', - 'Current', current_timestamp, 'infinity'::timestamptz + 'vl______wvl1', 'vault generic credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', + 'Not Applicable', 'Not Applicable', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', current_timestamp, 'infinity'::timestamptz ); update wh_credential_dimension @@ -30,20 +32,22 @@ begin; insert into wh_credential_dimension ( credential_purpose, - credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, - credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, - target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, - project_id, project_name, project_description, - organization_id, organization_name, organization_description, - current_row_indicator, row_effective_time, row_expiration_time + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time ) values ( 'brokered', - 'vl______wvl1', 'vault credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', - 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', - 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, - 'p____bwidget', 'Big Widget Factory', 'None', - 'o_____widget', 'Widget Inc', 'None', - 'Current', current_timestamp, 'infinity'::timestamptz + 'vl______wvl1', 'vault generic credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', + 'Not Applicable', 'Not Applicable', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', current_timestamp, 'infinity'::timestamptz ); select is(count(*), 2::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; diff --git a/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ecdsa.sql b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ecdsa.sql new file mode 100644 index 000000000..edf4f20cf --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ecdsa.sql @@ -0,0 +1,56 @@ +-- ssh_certificate_cred_library tests the wh_credential_dimension when +-- sessions are created with targets that use a vault ssh cert credential library with ecdsa key type +begin; + select plan(3); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert first session, should result in a new credentials dimension + insert into session + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____swidget', 't________ws2', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's2____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s2____walter', 's___1ws-sths', 'h_____ws__01'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s2____walter', 'vscl____wvl2', null, 'injected_application'); + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + prepare select_credential_dimensions as + select + credential_purpose::text, + credential_library_id::text, credential_library_type::text, credential_library_name::text, credential_library_description::text, credential_library_vault_path::text, credential_library_vault_http_method::text, credential_library_vault_http_request_body::text, + credential_library_username::text, credential_library_key_type_and_bits::text, + credential_store_id::text, credential_store_type::text, credential_store_name::text, credential_store_description::text, credential_store_vault_namespace::text, credential_store_vault_address::text, + target_id::text, target_type::text, target_name::text, target_description::text, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id::text, project_name::text, project_description::text, + organization_id::text, organization_name::text, organization_description::text, + current_row_indicator::text + from wh_credential_dimension + where organization_id = 'o_____widget'; + + select results_eq( + 'select_credential_dimensions', + $$VALUES + ( + 'injected_application', + 'vscl____wvl2', 'vault ssh certificate credential library', 'widget ssh ecdsa', 'None', '/ssh/sign/user', 'Not Applicable', 'Not Applicable', + 'user', 'ecdsa-521', + 'vs______swvs', 'vault credential store', 'small widget vault store', 'None', 'default', 'https://small.vault.widget', + 't________ws2', 'tcp target', 'Small Widget Target 2', 'None', 0, 28800, -1, + 'p____swidget', 'Small Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current' + )$$ + ); + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ed25519.sql b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ed25519.sql new file mode 100644 index 000000000..b62fd3049 --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ed25519.sql @@ -0,0 +1,56 @@ +-- ssh_certificate_cred_library tests the wh_credential_dimension when +-- sessions are created with targets that use a vault ssh cert credential library +begin; + select plan(3); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert first session, should result in a new credentials dimension + insert into session + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____swidget', 't________ws3', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's3____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s3____walter', 's___1ws-sths', 'h_____ws__01'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s3____walter', 'vscl____wvl3', null, 'injected_application'); + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + prepare select_credential_dimensions as + select + credential_purpose::text, + credential_library_id::text, credential_library_type::text, credential_library_name::text, credential_library_description::text, credential_library_vault_path::text, credential_library_vault_http_method::text, credential_library_vault_http_request_body::text, + credential_library_username::text, credential_library_key_type_and_bits::text, + credential_store_id::text, credential_store_type::text, credential_store_name::text, credential_store_description::text, credential_store_vault_namespace::text, credential_store_vault_address::text, + target_id::text, target_type::text, target_name::text, target_description::text, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id::text, project_name::text, project_description::text, + organization_id::text, organization_name::text, organization_description::text, + current_row_indicator::text + from wh_credential_dimension + where organization_id = 'o_____widget'; + + select results_eq( + 'select_credential_dimensions', + $$VALUES + ( + 'injected_application', + 'vscl____wvl3', 'vault ssh certificate credential library', 'widget ssh ed25519', 'None', '/ssh/sign/user', 'Not Applicable', 'Not Applicable', + 'user', 'ed25519', + 'vs______swvs', 'vault credential store', 'small widget vault store', 'None', 'default', 'https://small.vault.widget', + 't________ws3', 'tcp target', 'Small Widget Target 3', 'None', 0, 28800, -1, + 'p____swidget', 'Small Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current' + )$$ + ); + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_rsa.sql b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_rsa.sql new file mode 100644 index 000000000..c4506cbb5 --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_rsa.sql @@ -0,0 +1,56 @@ +-- ssh_certificate_cred_library tests the wh_credential_dimension when +-- sessions are created with targets that use a vault ssh cert credential library with rsa key type +begin; + select plan(3); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert first session, should result in a new credentials dimension + insert into session + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____swidget', 't_________ws', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1ws-sths', 'h_____ws__01'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s1____walter', 'vscl____wvl1', null, 'injected_application'); + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + prepare select_credential_dimensions as + select + credential_purpose::text, + credential_library_id::text, credential_library_type::text, credential_library_name::text, credential_library_description::text, credential_library_vault_path::text, credential_library_vault_http_method::text, credential_library_vault_http_request_body::text, + credential_library_username::text, credential_library_key_type_and_bits::text, + credential_store_id::text, credential_store_type::text, credential_store_name::text, credential_store_description::text, credential_store_vault_namespace::text, credential_store_vault_address::text, + target_id::text, target_type::text, target_name::text, target_description::text, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id::text, project_name::text, project_description::text, + organization_id::text, organization_name::text, organization_description::text, + current_row_indicator::text + from wh_credential_dimension + where organization_id = 'o_____widget'; + + select results_eq( + 'select_credential_dimensions', + $$VALUES + ( + 'injected_application', + 'vscl____wvl1', 'vault ssh certificate credential library', 'widget ssh admin cert', 'None', '/ssh/issue/admin', 'Not Applicable', 'Not Applicable', + 'admin', 'rsa-4096', + 'vs______swvs', 'vault credential store', 'small widget vault store', 'None', 'default', 'https://small.vault.widget', + 't_________ws', 'tcp target', 'Small Widget Target', 'None', 0, 28800, -1, + 'p____swidget', 'Small Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current' + )$$ + ); + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql b/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql index 91e728474..8b5f39515 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql @@ -18,36 +18,39 @@ begin; select is(s.*, row( 's1____walter', - 'brokered', -- credential_purpose, - 'vl______wvl1', -- credential_library_id, - 'vault credential library', -- credential_library_type, - 'widget vault library', -- credential_library_name, - 'None', -- credential_library_description, - '/secrets', -- credential_library_vault_path, - 'GET', -- credential_library_vault_http_method, - 'None', -- credential_library_vault_http_request_body, + 'brokered', -- credential_purpose, + 'vl______wvl1', -- credential_library_id, + 'vault generic credential library', -- credential_library_type, + 'widget vault library', -- credential_library_name, + 'None', -- credential_library_description, + '/secrets', -- credential_library_vault_path, + 'GET', -- credential_library_vault_http_method, + 'Not Applicable', -- credential_library_vault_http_request_body, - 'vs_______wvs', -- credential_store_id, - 'vault credential store', -- credential_store_type, - 'widget vault store', -- credential_store_name, - 'None', -- credential_store_description, - 'default', -- credential_store_vault_namespace, - 'https://vault.widget', -- credential_store_vault_address, + 'Not Applicable', -- credential_library_username, + 'Not Applicable', -- credential_library_key_type_and_bits, - 't_________wb', -- target_id, - 'tcp target', -- target_type, - 'Big Widget Target', -- target_name, - 'None', -- target_description, - 0, -- target_default_port_number, - 28800, -- target_session_max_seconds, - -1, -- target_session_connection_limit, + 'vs_______wvs', -- credential_store_id, + 'vault credential store', -- credential_store_type, + 'widget vault store', -- credential_store_name, + 'None', -- credential_store_description, + 'default', -- credential_store_vault_namespace, + 'https://vault.widget', -- credential_store_vault_address, - 'p____bwidget', -- project_id, - 'Big Widget Factory', -- project_name, - 'None', -- project_description, - 'o_____widget', -- organization_id, - 'Widget Inc', -- organization_name, - 'None' -- organization_description + 't_________wb', -- target_id, + 'tcp target', -- target_type, + 'Big Widget Target', -- target_name, + 'None', -- target_description, + 0, -- target_default_port_number, + 28800, -- target_session_max_seconds, + -1, -- target_session_connection_limit, + + 'p____bwidget', -- project_id, + 'Big Widget Factory', -- project_name, + 'None', -- project_description, + 'o_____widget', -- organization_id, + 'Widget Inc', -- organization_name, + 'None' -- organization_description )::whx_credential_dimension_source) from whx_credential_dimension_source as s where s.target_id = 't_________wb'; diff --git a/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql b/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql index 67ba4bd45..26e066681 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql @@ -8,33 +8,36 @@ begin; ( key, credential_purpose, - credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, - credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, - target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, - project_id, project_name, project_description, - organization_id, organization_name, organization_description, - current_row_indicator, row_effective_time, row_expiration_time + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time ) values ( 'wcd________1', 'brokered', - 'vl_______wvl', 'vault credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', - 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', - 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, - 'p____bwidget', 'Big Widget Factory', 'None', - 'o_____widget', 'Widget Inc', 'None', - 'Current', '2021-07-21T12:01'::timestamptz, 'infinity'::timestamptz + 'vl_______wvl', 'vault generic credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', + 'Not Applicable', 'Not Applicable', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', '2021-07-21T12:01'::timestamptz, 'infinity'::timestamptz ); select is(t.*, row( 'wcd________1', 'brokered', - 'vl_______wvl', 'vault credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', - 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', - 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, - 'p____bwidget', 'Big Widget Factory', 'None', - 'o_____widget', 'Widget Inc', 'None' + 'vl_______wvl', 'vault generic credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', + 'Not Applicable', 'Not Applicable', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None' )::whx_credential_dimension_target) from whx_credential_dimension_target as t where t.target_id = 't_________wb'; From 1aaaf4af1d68b57f426bae8ab1216c5180695796 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Tue, 31 Jan 2023 14:07:06 +0000 Subject: [PATCH 08/11] feat(controller): Translate ssh certificate credentials for worker --- .../handlers/targets/credentials.go | 11 +- .../servers/services/credential.pb.go | 164 +++++++++++++++--- .../servers/services/v1/credential.proto | 14 ++ 3 files changed, 161 insertions(+), 28 deletions(-) diff --git a/internal/daemon/controller/handlers/targets/credentials.go b/internal/daemon/controller/handlers/targets/credentials.go index f68097150..ad8f1184c 100644 --- a/internal/daemon/controller/handlers/targets/credentials.go +++ b/internal/daemon/controller/handlers/targets/credentials.go @@ -33,7 +33,16 @@ func dynamicToWorkerCredential(ctx context.Context, cred credential.Dynamic) (se }, }, } - + case credential.SshCertificate: + workerCred = &serverpb.Credential{ + Credential: &serverpb.Credential_SshCertificate{ + SshCertificate: &serverpb.SshCertificate{ + Username: c.Username(), + PrivateKey: string(c.PrivateKey()), + Certificate: string(c.Certificate()), + }, + }, + } case credential.SshPrivateKey: workerCred = &serverpb.Credential{ Credential: &serverpb.Credential_SshPrivateKey{ diff --git a/internal/gen/controller/servers/services/credential.pb.go b/internal/gen/controller/servers/services/credential.pb.go index 6edb422ab..2d310c0a4 100644 --- a/internal/gen/controller/servers/services/credential.pb.go +++ b/internal/gen/controller/servers/services/credential.pb.go @@ -29,6 +29,7 @@ type Credential struct { // // *Credential_UsernamePassword // *Credential_SshPrivateKey + // *Credential_SshCertificate Credential isCredential_Credential `protobuf_oneof:"credential"` } @@ -85,6 +86,13 @@ func (x *Credential) GetSshPrivateKey() *SshPrivateKey { return nil } +func (x *Credential) GetSshCertificate() *SshCertificate { + if x, ok := x.GetCredential().(*Credential_SshCertificate); ok { + return x.SshCertificate + } + return nil +} + type isCredential_Credential interface { isCredential_Credential() } @@ -97,10 +105,16 @@ type Credential_SshPrivateKey struct { SshPrivateKey *SshPrivateKey `protobuf:"bytes,3,opt,name=ssh_private_key,json=sshPrivateKey,proto3,oneof"` } +type Credential_SshCertificate struct { + SshCertificate *SshCertificate `protobuf:"bytes,4,opt,name=ssh_certificate,json=sshCertificate,proto3,oneof"` +} + func (*Credential_UsernamePassword) isCredential_Credential() {} func (*Credential_SshPrivateKey) isCredential_Credential() {} +func (*Credential_SshCertificate) isCredential_Credential() {} + // UsernamePassword is a credential containing a username and a password. type UsernamePassword struct { state protoimpl.MessageState @@ -227,6 +241,74 @@ func (x *SshPrivateKey) GetPrivateKeyPassphrase() string { return "" } +// SshCertificate is a credential containing a username, private key, and +// client certificate. +type SshCertificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The username of the credential + Username string `protobuf:"bytes,10,opt,name=username,proto3" json:"username,omitempty"` // @gotags: `class:"public"` + // The private key of the credential + PrivateKey string `protobuf:"bytes,20,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` // @gotags: `class:"secret"` + // The client certificate signed by a CA to establish trust of the private key. + Certificate string `protobuf:"bytes,30,opt,name=certificate,proto3" json:"certificate,omitempty"` // @gotags: `class:"public"` +} + +func (x *SshCertificate) Reset() { + *x = SshCertificate{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_servers_services_v1_credential_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SshCertificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshCertificate) ProtoMessage() {} + +func (x *SshCertificate) ProtoReflect() protoreflect.Message { + mi := &file_controller_servers_services_v1_credential_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SshCertificate.ProtoReflect.Descriptor instead. +func (*SshCertificate) Descriptor() ([]byte, []int) { + return file_controller_servers_services_v1_credential_proto_rawDescGZIP(), []int{3} +} + +func (x *SshCertificate) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *SshCertificate) GetPrivateKey() string { + if x != nil { + return x.PrivateKey + } + return "" +} + +func (x *SshCertificate) GetCertificate() string { + if x != nil { + return x.Certificate + } + return "" +} + var File_controller_servers_services_v1_credential_proto protoreflect.FileDescriptor var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{ @@ -235,7 +317,7 @@ var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{ 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x22, 0xe9, 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x31, 0x22, 0xc4, 0x02, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x5f, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, @@ -247,28 +329,41 @@ var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{ 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x0d, 0x73, 0x73, 0x68, - 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x0d, - 0x75, 0x73, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4a, 0x0a, - 0x10, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x82, 0x01, 0x0a, 0x0d, 0x53, 0x73, - 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x75, + 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x59, 0x0a, 0x0f, 0x73, 0x73, + 0x68, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x73, 0x68, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x73, 0x68, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4a, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x22, 0x82, 0x01, 0x0a, 0x0d, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x4b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x18, 0x1e, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x14, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, + 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x22, 0x6f, 0x0a, 0x0e, 0x53, 0x73, 0x68, + 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, - 0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x42, 0x51, - 0x5a, 0x4f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x42, 0x51, 0x5a, 0x4f, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -283,20 +378,22 @@ func file_controller_servers_services_v1_credential_proto_rawDescGZIP() []byte { return file_controller_servers_services_v1_credential_proto_rawDescData } -var file_controller_servers_services_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_servers_services_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_controller_servers_services_v1_credential_proto_goTypes = []interface{}{ (*Credential)(nil), // 0: controller.servers.services.v1.Credential (*UsernamePassword)(nil), // 1: controller.servers.services.v1.UsernamePassword (*SshPrivateKey)(nil), // 2: controller.servers.services.v1.SshPrivateKey + (*SshCertificate)(nil), // 3: controller.servers.services.v1.SshCertificate } var file_controller_servers_services_v1_credential_proto_depIdxs = []int32{ 1, // 0: controller.servers.services.v1.Credential.username_password:type_name -> controller.servers.services.v1.UsernamePassword 2, // 1: controller.servers.services.v1.Credential.ssh_private_key:type_name -> controller.servers.services.v1.SshPrivateKey - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 3, // 2: controller.servers.services.v1.Credential.ssh_certificate:type_name -> controller.servers.services.v1.SshCertificate + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_controller_servers_services_v1_credential_proto_init() } @@ -341,10 +438,23 @@ func file_controller_servers_services_v1_credential_proto_init() { return nil } } + file_controller_servers_services_v1_credential_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SshCertificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_servers_services_v1_credential_proto_msgTypes[0].OneofWrappers = []interface{}{ (*Credential_UsernamePassword)(nil), (*Credential_SshPrivateKey)(nil), + (*Credential_SshCertificate)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -352,7 +462,7 @@ func file_controller_servers_services_v1_credential_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_servers_services_v1_credential_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/proto/controller/servers/services/v1/credential.proto b/internal/proto/controller/servers/services/v1/credential.proto index 8943defff..7ca4a4358 100644 --- a/internal/proto/controller/servers/services/v1/credential.proto +++ b/internal/proto/controller/servers/services/v1/credential.proto @@ -11,6 +11,7 @@ message Credential { oneof credential { UsernamePassword username_password = 2; SshPrivateKey ssh_private_key = 3; + SshCertificate ssh_certificate = 4; } } @@ -35,3 +36,16 @@ message SshPrivateKey { // The optional passphrase of the private_key string private_key_passphrase = 30; // @gotags: `class:"secret"` } + +// SshCertificate is a credential containing a username, private key, and +// client certificate. +message SshCertificate { + // The username of the credential + string username = 10; // @gotags: `class:"public"` + + // The private key of the credential + string private_key = 20; // @gotags: `class:"secret"` + + // The client certificate signed by a CA to establish trust of the private key. + string certificate = 30; // @gotags: `class:"public"` +} From 12a53bbfa90c79ef776e5e3674e9b250c6638156 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Tue, 31 Jan 2023 15:30:18 +0000 Subject: [PATCH 09/11] feat(target): Allow vault ssh cert library as credential source This allows the new vault ssh certificate credential library to be used as an injected application credential source. --- internal/daemon/controller/handlers/targets/target_service.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 0d18ee19f..a1735b179 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -1810,6 +1810,7 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ for _, cl := range req.GetInjectedApplicationCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, + vault.SSHCertificateCredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix) { @@ -1857,6 +1858,7 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ for _, cl := range req.GetInjectedApplicationCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, + vault.SSHCertificateCredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix) { @@ -1908,6 +1910,7 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc for _, cl := range req.GetInjectedApplicationCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, + vault.SSHCertificateCredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix, From 32dd7f07d036ae4563999cebf7f3e818a7f5063d Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Tue, 31 Jan 2023 22:14:56 +0000 Subject: [PATCH 10/11] fix(vault): Only persist revokable credentials When issuing credentials from vault credential libraries, only persist the vault credential if it is revokable. A credential that is revokable has a valid lease id that can be used to revoke the lease in vault. If the credential is not revokable it is also not renewable since renewing also requires a lease id. See: https://developer.hashicorp.com/vault/docs/concepts/lease --- internal/credential/vault/private_library.go | 3 + .../vault/repository_credentials.go | 8 +- .../vault/repository_credentials_test.go | 129 +++++++++++------- 3 files changed, 89 insertions(+), 51 deletions(-) diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index a161f4ce1..8b99947d5 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/internal/sshprivatekey" "github.com/hashicorp/boundary/internal/credential/vault/internal/usernamepassword" + "github.com/hashicorp/boundary/internal/db/sentinel" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" @@ -46,6 +47,7 @@ func (bc *baseCred) Library() credential.Library { return bc.lib } func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.GetPurpose() } func (bc *baseCred) getExpiration() time.Duration { return bc.expiration } func (bc *baseCred) getCredential() *Credential { return bc.Credential } +func (bc *baseCred) isRevokable() bool { return bc.ExternalId != sentinel.ExternalIdNone } // convert converts bc to a specific credential type if bc is not // UnspecifiedType. @@ -289,6 +291,7 @@ type dynamicCred interface { credential.Dynamic getExpiration() time.Duration getCredential() *Credential + isRevokable() bool } // retrieveCredential retrieves a dynamic credential from Vault for the diff --git a/internal/credential/vault/repository_credentials.go b/internal/credential/vault/repository_credentials.go index ab715847e..f84a824d6 100644 --- a/internal/credential/vault/repository_credentials.go +++ b/internal/credential/vault/repository_credentials.go @@ -75,6 +75,12 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre return nil, err } + creds = append(creds, cred) + if !cred.isRevokable() { + // No need to persist since the credential cannot be revoked nor renewed + continue + } + if cred.getExpiration() < runJobsInterval { event.WriteError(ctx, op, fmt.Errorf("WARNING: credential will expire before job scheduler can run"), @@ -123,8 +129,6 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre ); err != nil { return nil, errors.Wrap(ctx, err, op) } - - creds = append(creds, cred) } // Best effort update next run time of credential renewal job, but an error should not diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 0d66bf1b9..5fe236f4d 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -23,7 +23,7 @@ import ( ) // since we're not using gorm tags to retrieve this, it's faster and easier to just make a new struct than import usrPassCred from the vault package -type userPassCred struct { +type revokableCred struct { PublicId string LibraryId string SessionId string @@ -35,7 +35,7 @@ type userPassCred struct { ExpirationTime *timestamp.Timestamp } -func lookupDbCred(t *testing.T, ctx context.Context, rw *db.Db, upc credential.UsernamePassword) userPassCred { +func lookupDbCred(t *testing.T, ctx context.Context, rw *db.Db, dc credential.Dynamic) *revokableCred { rows, err := rw.Query(ctx, ` select public_id, @@ -49,11 +49,11 @@ func lookupDbCred(t *testing.T, ctx context.Context, rw *db.Db, upc credential.U expiration_time from credential_vault_credential where public_id = ?; - `, []any{upc.GetPublicId()}) + `, []any{dc.GetPublicId()}) require.NoError(t, err) rowCount := 0 - got := userPassCred{} + got := revokableCred{} for rows.Next() { rowCount++ @@ -69,9 +69,46 @@ func lookupDbCred(t *testing.T, ctx context.Context, rw *db.Db, upc credential.U &got.ExpirationTime, )) } - assert.Equal(t, 1, rowCount) + // Should never get more than one that matches, but can get 0 + assert.LessOrEqual(t, rowCount, 1) - return got + if rowCount == 0 { + return nil + } + + return &got +} + +type libT int + +const ( + libDB libT = iota + libUsrPassDB + libErrUsrPassDB + libPKI + libErrPKI + libKV + libErrKV + libUsrPassKV + libSshPkKV + libExpiredToken +) + +type testLib struct { + PublicId string + HasLease bool + IsRenewable bool +} + +type testLibMap map[libT]testLib + +func (m testLibMap) GetByPublicId(id string) (testLib, bool) { + for _, v := range m { + if v.PublicId == id { + return v, true + } + } + return testLib{}, false } func TestRepository_IssueCredentials(t *testing.T) { @@ -133,21 +170,7 @@ func TestRepository_IssueCredentials(t *testing.T) { assert.Equal(t, 1, num) v.RevokeToken(t, expToken) - type libT int - const ( - libDB libT = iota - libUsrPassDB - libErrUsrPassDB - libPKI - libErrPKI - libKV - libErrKV - libUsrPassKV - libSshPkKV - libExpiredToken - ) - - libs := make(map[libT]string) + libs := make(testLibMap) { libPath := path.Join("database", "creds", "opened") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath) @@ -156,7 +179,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libDB] = lib.GetPublicId() + libs[libDB] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true} } { libPath := path.Join("pki", "issue", "boundary") @@ -166,7 +189,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libPKI] = lib.GetPublicId() + libs[libPKI] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: false} } { @@ -177,7 +200,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libErrPKI] = lib.GetPublicId() + libs[libErrPKI] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true} } { libPath := path.Join("secret", "data", "my-up-secret") @@ -187,7 +210,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libKV] = lib.GetPublicId() + libs[libKV] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: true} } { libPath := path.Join("secret", "data", "fake-secret") @@ -197,7 +220,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libErrKV] = lib.GetPublicId() + libs[libErrKV] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: true} } { libPath := path.Join("database", "creds", "opened") @@ -210,7 +233,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libUsrPassDB] = lib.GetPublicId() + libs[libUsrPassDB] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true} } { libPath := path.Join("database", "creds", "opened") @@ -227,7 +250,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libErrUsrPassDB] = lib.GetPublicId() + libs[libErrUsrPassDB] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true} } { libPath := path.Join("secret", "data", "my-up-secret") @@ -240,7 +263,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libUsrPassKV] = lib.GetPublicId() + libs[libUsrPassKV] = testLib{PublicId: lib.GetPublicId(), HasLease: false} } { libPath := path.Join("secret", "data", "my-sshpk-secret") @@ -253,7 +276,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libSshPkKV] = lib.GetPublicId() + libs[libSshPkKV] = testLib{PublicId: lib.GetPublicId(), HasLease: false} } { libPath := path.Join("secret", "data", "my-up-secret") @@ -266,7 +289,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libExpiredToken] = lib.GetPublicId() + libs[libExpiredToken] = testLib{PublicId: lib.GetPublicId(), HasLease: false} } at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) @@ -303,7 +326,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libDB], + SourceId: libs[libDB].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -313,11 +336,11 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libDB], + SourceId: libs[libDB].PublicId, Purpose: credential.BrokeredPurpose, }, { - SourceId: libs[libPKI], + SourceId: libs[libPKI].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -327,7 +350,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libErrPKI], + SourceId: libs[libErrPKI].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -338,11 +361,11 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2nil, requests: []credential.Request{ { - SourceId: libs[libDB], + SourceId: libs[libDB].PublicId, Purpose: credential.BrokeredPurpose, }, { - SourceId: libs[libPKI], + SourceId: libs[libPKI].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -353,7 +376,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libKV], + SourceId: libs[libKV].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -363,7 +386,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libUsrPassDB], + SourceId: libs[libUsrPassDB].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -373,7 +396,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libErrKV], + SourceId: libs[libErrKV].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -384,7 +407,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libErrUsrPassDB], + SourceId: libs[libErrUsrPassDB].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -395,7 +418,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libUsrPassKV], + SourceId: libs[libUsrPassKV].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -405,7 +428,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libSshPkKV], + SourceId: libs[libSshPkKV].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -415,7 +438,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libExpiredToken], + SourceId: libs[libExpiredToken].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -451,11 +474,6 @@ func TestRepository_IssueCredentials(t *testing.T) { if upc, ok := dc.(credential.UsernamePassword); ok { assert.NotEmpty(upc.Username()) assert.NotEmpty(upc.Password()) - // we also want to retrieve the cred from the db and make sure it's the same as the one returned by Issue - retrieved := lookupDbCred(t, ctx, rw, upc) - require.Equal(retrieved.SessionId, sess.GetPublicId()) - require.Equal(retrieved.PublicId, upc.GetPublicId()) - // these asserts are super basic because the main check we want is that lookupDbCred succeeded break } assert.Fail("want UsernamePassword credential from library with credential type UsernamePassword") @@ -464,6 +482,19 @@ func TestRepository_IssueCredentials(t *testing.T) { assert.Fail("do not want UsernamePassword credential from library with credential type Unspecified") } } + if lib, ok := libs.GetByPublicId(dc.Library().GetPublicId()); ok { + retrieved := lookupDbCred(t, ctx, rw, dc) + if lib.HasLease { + // we also want to retrieve the cred from the db and make sure it's the same as the one returned by Issue + require.NotNil(retrieved) + require.Equal(retrieved.SessionId, sess.GetPublicId()) + require.Equal(retrieved.PublicId, dc.GetPublicId()) + assert.NotEmpty(retrieved.ExternalId) + assert.Equal(retrieved.IsRenewable, lib.IsRenewable) + } else { + assert.Nil(retrieved) + } + } } }) } From 862481960d86f8a05ba29ac4f4fb736a762a1278 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Thu, 2 Feb 2023 15:06:10 +0000 Subject: [PATCH 11/11] chore(CHANGELOG): Add items for vault ssh certificate cred library --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 925e0d18d..fcbbbda23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,22 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. 0.14.0, this will become an error instead. As a consequence, it means that the fallback logic for targets that did not have a default port defined is no longer in service; all targets must now have a default port defined. +* With the introduction of `vault-ssh-certificate` credential libraries, the + `vault` credential library subtype is being renamed to `vault-generic` to + denote it as a credential library that can be used in a generalized way to + issue credentials from vault. Existing credential libraries with the + subtype of `vault` will be updated to `vault-generic`. The subtype of + `vault` will still be accepted as a valid subtype in API requests to the + credential libraries endpoints, but is deprecated. Instead `vault-generic` + should be used. In addition the `boundary credential-libraries create + vault` and `boundary credential-libraries update vault` subcommands will + still function, but are deprecated. Instead `boundary credential-libraries + create vault-generic` and `boundary credential-libraries update + vault-generic` should be used. Also note that any credential library created + using the subtype of `vault`, either via the API or via the deprecated + subcommand, will have the subtype set to `vault-generic`. The deprecated + subtype and subcommands will be removed in boundary 0.14.0, at which point + `vault-generic` must be used. ### New and Improved @@ -39,6 +55,10 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. multiple workers together to access services hidden under layers of network security. Multi-hop workers can also establish a TCP session through multiple workers, with the ability to reverse proxy and establish a connection. +* Vault SSH certificate credential library: A new credential library that uses + the vault ssh secret engine to generate ssh private key and certificates. The + library can be used as an injected application credential source for targets + that support credential injection. ([PR](https://github.com/hashicorp/boundary/pull/2860)) ### Bug Fixes