From ef5ac07f0223d66743e3f695514e2b09558c0e9c Mon Sep 17 00:00:00 2001 From: Louis Ruch Date: Tue, 19 Jul 2022 10:31:53 -0700 Subject: [PATCH] Add ssh_private_key support for Vault libraries and targets (#2263) * Add support for ssh_private_key credential type in Vault libraries * Add support for ssh_private_key credential type * Support jsonpointer for Vault library mapping overrides --- internal/credential/credential.go | 14 +- .../static/repository_credentials.go | 29 +- .../static/repository_credentials_test.go | 58 ++- .../vault/credential_library_test.go | 33 +- .../vault/internal/sshprivatekey/doc.go | 3 + .../internal/sshprivatekey/sshprivatekey.go | 135 ++++++ .../sshprivatekey/sshprivatekey_test.go | 274 +++++++++++ .../usernamepassword/usernamepassword.go | 56 ++- .../usernamepassword/usernamepassword_test.go | 46 ++ .../credential/vault/mapping_overriders.go | 65 +++ .../vault/mapping_overriders_test.go | 10 + internal/credential/vault/options.go | 15 +- internal/credential/vault/options_test.go | 6 + internal/credential/vault/private_library.go | 179 +++++--- .../credential/vault/private_library_test.go | 434 ++++++++++++++++++ .../vault/repository_credential_library.go | 54 ++- .../repository_credential_library_test.go | 361 ++++++++++++++- .../vault/repository_credentials_test.go | 33 +- internal/credential/vault/store/vault.pb.go | 147 +++++- .../credentiallibrary_service.go | 51 +- .../credentiallibrary_service_test.go | 259 +++++++++++ .../handlers/targets/credentials.go | 53 ++- .../handlers/targets/target_service.go | 36 +- .../targets/tcp/target_service_test.go | 146 +++++- .../36/02_vault_private_library.up.sql | 3 +- .../02_vault_ssh_private_key_override.up.sql | 114 +++++ .../sqltest/initdb.d/03_widgets_persona.sql | 41 +- ...brary_ssh_private_key_mapping_override.sql | 61 +++ .../servers/services/credential.pb.go | 140 +++++- .../api/resources/targets/v1/target.proto | 9 + .../servers/services/v1/credential.proto | 10 + .../credential/vault/store/v1/vault.proto | 26 ++ .../api/resources/targets/target.pb.go | 138 ++++-- 33 files changed, 2805 insertions(+), 234 deletions(-) create mode 100644 internal/credential/vault/internal/sshprivatekey/doc.go create mode 100644 internal/credential/vault/internal/sshprivatekey/sshprivatekey.go create mode 100644 internal/credential/vault/internal/sshprivatekey/sshprivatekey_test.go create mode 100644 internal/db/schema/migrations/oss/postgres/39/02_vault_ssh_private_key_override.up.sql create mode 100644 internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 074044f81..87ca35bc6 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -132,17 +132,9 @@ type UsernamePassword interface { Password() Password } -// KeyPair is a credential containing a username and a private key. -type KeyPair interface { +// SshPrivateKey is a credential containing a username and a SSH private key. +type SshPrivateKey interface { Credential Username() string - Private() PrivateKey -} - -// Certificate is a credential containing a certificate and the private key -// for the certificate. -type Certificate interface { - Credential - Certificate() []byte - Private() PrivateKey + PrivateKey() PrivateKey } diff --git a/internal/credential/static/repository_credentials.go b/internal/credential/static/repository_credentials.go index de3edae83..ec1caf135 100644 --- a/internal/credential/static/repository_credentials.go +++ b/internal/credential/static/repository_credentials.go @@ -17,18 +17,37 @@ func (r *Repository) Retrieve(ctx context.Context, scopeId string, ids []string) return nil, errors.New(ctx, errors.InvalidParameter, op, "no ids") } - var creds []*UsernamePasswordCredential - err := r.reader.SearchWhere(ctx, &creds, "public_id in (?)", []interface{}{ids}) + var upCreds []*UsernamePasswordCredential + err := r.reader.SearchWhere(ctx, &upCreds, "public_id in (?)", []interface{}{ids}) if err != nil { return nil, errors.Wrap(ctx, err, op) } - if len(creds) != len(ids) { + var spkCreds []*SshPrivateKeyCredential + err = r.reader.SearchWhere(ctx, &spkCreds, "public_id in (?)", []interface{}{ids}) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + if len(upCreds)+len(spkCreds) != len(ids) { return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, - fmt.Sprintf("mismatch between creds and number of ids requested, expected %d got %d", len(ids), len(creds))) + fmt.Sprintf("mismatch between creds and number of ids requested, expected %d got %d", len(ids), len(upCreds)+len(spkCreds))) } out := make([]credential.Static, 0, len(ids)) - for _, c := range creds { + for _, c := range upCreds { + // decrypt credential + databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.decrypt(ctx, databaseWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + out = append(out, c) + } + + for _, c := range spkCreds { // decrypt credential databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) if err != nil { diff --git a/internal/credential/static/repository_credentials_test.go b/internal/credential/static/repository_credentials_test.go index 64374090a..c59d061a8 100644 --- a/internal/credential/static/repository_credentials_test.go +++ b/internal/credential/static/repository_credentials_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh/testdata" ) func TestRepository_Retrieve(t *testing.T) { @@ -30,9 +31,10 @@ func TestRepository_Retrieve(t *testing.T) { org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) staticStore := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) - cred1 := TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", staticStore.GetPublicId(), prj.GetPublicId()) - cred2 := TestUsernamePasswordCredential(t, conn, wrapper, "different user", "better password", staticStore.GetPublicId(), prj.GetPublicId()) - cred3 := TestUsernamePasswordCredential(t, conn, wrapper, "final user", "horrible password", staticStore.GetPublicId(), prj.GetPublicId()) + upCred1 := TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", staticStore.GetPublicId(), prj.GetPublicId()) + upCred2 := TestUsernamePasswordCredential(t, conn, wrapper, "different user", "better password", staticStore.GetPublicId(), prj.GetPublicId()) + spkCred1 := TestSshPrivateKeyCredential(t, conn, wrapper, "final user", string(testdata.PEMBytes["ed25519"]), staticStore.GetPublicId(), prj.GetPublicId()) + spkCred2 := TestSshPrivateKeyCredential(t, conn, wrapper, "last user", string(testdata.PEMBytes["rsa-openssh-format"]), staticStore.GetPublicId(), prj.GetPublicId()) type args struct { credIds []string @@ -47,7 +49,7 @@ func TestRepository_Retrieve(t *testing.T) { { name: "no-scope", args: args{ - credIds: []string{cred1.GetPublicId()}, + credIds: []string{upCred1.GetPublicId()}, }, wantErr: true, }, @@ -55,28 +57,58 @@ func TestRepository_Retrieve(t *testing.T) { name: "invalid-scope", args: args{ scopeId: org.GetPublicId(), - credIds: []string{cred1.GetPublicId()}, + credIds: []string{upCred1.GetPublicId()}, }, wantErr: true, }, { - name: "valid-one-cred", + name: "valid-one-up-cred", args: args{ scopeId: prj.GetPublicId(), - credIds: []string{cred1.GetPublicId()}, + credIds: []string{upCred1.GetPublicId()}, }, wantCreds: []credential.Static{ - cred1, + upCred1, }, }, { - name: "valid-multiple-creds", + name: "valid-multiple-up-creds", args: args{ scopeId: prj.GetPublicId(), - credIds: []string{cred1.GetPublicId(), cred2.GetPublicId(), cred3.GetPublicId()}, + credIds: []string{upCred1.GetPublicId(), upCred2.GetPublicId()}, }, wantCreds: []credential.Static{ - cred1, cred2, cred3, + upCred1, upCred2, + }, + }, + { + name: "valid-ssh-pk-cred", + args: args{ + scopeId: prj.GetPublicId(), + credIds: []string{spkCred1.GetPublicId()}, + }, + wantCreds: []credential.Static{ + spkCred1, + }, + }, + { + name: "valid-multiple-ssh-pk-creds", + args: args{ + scopeId: prj.GetPublicId(), + credIds: []string{spkCred1.GetPublicId(), spkCred2.GetPublicId()}, + }, + wantCreds: []credential.Static{ + spkCred1, spkCred2, + }, + }, + { + name: "valid-mixed-creds", + args: args{ + scopeId: prj.GetPublicId(), + credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId()}, + }, + wantCreds: []credential.Static{ + upCred1, spkCred1, spkCred2, upCred2, }, }, } @@ -94,7 +126,9 @@ func TestRepository_Retrieve(t *testing.T) { cmp.Diff( tt.wantCreds, gotCreds, - cmpopts.IgnoreUnexported(UsernamePasswordCredential{}, store.UsernamePasswordCredential{}), + cmpopts.IgnoreUnexported( + UsernamePasswordCredential{}, store.UsernamePasswordCredential{}, + SshPrivateKeyCredential{}, store.SshPrivateKeyCredential{}), cmpopts.IgnoreTypes(×tamp.Timestamp{}), cmpopts.SortSlices(func(x, y credential.Static) bool { return x.GetPublicId() < y.GetPublicId() diff --git a/internal/credential/vault/credential_library_test.go b/internal/credential/vault/credential_library_test.go index 83ad19d60..bf380ca50 100644 --- a/internal/credential/vault/credential_library_test.go +++ b/internal/credential/vault/credential_library_test.go @@ -234,11 +234,14 @@ func TestCredentialLibrary_New(t *testing.T) { opts: []Option{ WithMethod(MethodGet), WithCredentialType(credential.UsernamePasswordType), - WithMappingOverride(NewUsernamePasswordOverride(WithOverrideUsernameAttribute("test"))), + WithMappingOverride(NewUsernamePasswordOverride( + WithOverrideUsernameAttribute("test"), + WithOverridePasswordAttribute("testpass")), + ), }, }, want: &CredentialLibrary{ - MappingOverride: NewUsernamePasswordOverride(WithOverrideUsernameAttribute("test")), + MappingOverride: NewUsernamePasswordOverride(WithOverrideUsernameAttribute("test"), WithOverridePasswordAttribute("testpass")), CredentialLibrary: &store.CredentialLibrary{ StoreId: cs.PublicId, VaultPath: "vault/path", @@ -247,6 +250,30 @@ func TestCredentialLibrary_New(t *testing.T) { }, }, }, + { + name: "credential-type-with-ssh-pk-mapping", + args: args{ + storeId: cs.PublicId, + vaultPath: "vault/path", + opts: []Option{ + WithMethod(MethodGet), + WithCredentialType(credential.SshPrivateKeyType), + WithMappingOverride(NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("test"), + WithOverridePrivateKeyAttribute("testpk")), + ), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride(WithOverrideUsernameAttribute("test"), WithOverridePrivateKeyAttribute("testpk")), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.PublicId, + VaultPath: "vault/path", + HttpMethod: string(MethodGet), + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, } for _, tt := range tests { @@ -264,6 +291,8 @@ func TestCredentialLibrary_New(t *testing.T) { switch ct := tt.want.GetCredentialType(); ct { case string(credential.UsernamePasswordType): assert.Equal(credential.UsernamePasswordType, got.CredentialType()) + case string(credential.SshPrivateKeyType): + assert.Equal(credential.SshPrivateKeyType, got.CredentialType()) case string(credential.UnspecifiedType), "": assert.Equal(credential.UnspecifiedType, got.CredentialType()) default: diff --git a/internal/credential/vault/internal/sshprivatekey/doc.go b/internal/credential/vault/internal/sshprivatekey/doc.go new file mode 100644 index 000000000..ef8ad2316 --- /dev/null +++ b/internal/credential/vault/internal/sshprivatekey/doc.go @@ -0,0 +1,3 @@ +// Package sshprivatekey provides access to the username and ssh private key +// stored in a Vault secret. +package sshprivatekey diff --git a/internal/credential/vault/internal/sshprivatekey/sshprivatekey.go b/internal/credential/vault/internal/sshprivatekey/sshprivatekey.go new file mode 100644 index 000000000..18a89c378 --- /dev/null +++ b/internal/credential/vault/internal/sshprivatekey/sshprivatekey.go @@ -0,0 +1,135 @@ +package sshprivatekey + +import ( + "strings" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/mitchellh/pointerstructure" +) + +type ( + data map[string]any + + // extractFunc attempts to extract the username and private key + // from sd using the provided attribute names, using a known + // Vault data response format. + extractFunc func(sd data, usernameAttr, privateKeyAttr string) (string, credential.PrivateKey) +) + +// Extract attempts to extract the values of the username and private key +// stored within the provided data using the given attribute names. +// +// Extract does not return partial results, i.e. if one of the attributes +// were extracted but not the other ("", nil) will be returned. +func Extract(d data, usernameAttr, privateKeyAttr string) (string, credential.PrivateKey) { + for _, f := range []extractFunc{ + defaultExtract, + kv2Extract, + } { + username, privateKey := f(d, usernameAttr, privateKeyAttr) + if username != "" && privateKey != nil { + // got valid username and privateKey from secret + return username, privateKey + } + } + + return "", nil +} + +// defaultExtract looks for the usernameAttr and privateKeyAttr in the data map +func defaultExtract(sd data, usernameAttr, privateKeyAttr string) (username string, privateKey credential.PrivateKey) { + if sd == nil { + // nothing to do return early + return "", nil + } + + var u any + switch { + case strings.HasPrefix(usernameAttr, "/"): + var err error + u, err = pointerstructure.Get(sd, usernameAttr) + if err != nil { + return "", nil + } + + default: + u = sd[usernameAttr] + } + if u, ok := u.(string); ok { + username = u + } + + var p any + switch { + case strings.HasPrefix(privateKeyAttr, "/"): + var err error + p, err = pointerstructure.Get(sd, privateKeyAttr) + if err != nil { + return "", nil + } + + default: + p = sd[privateKeyAttr] + } + + if p, ok := p.(string); ok { + privateKey = credential.PrivateKey(p) + } + + return username, privateKey +} + +// kv2Extract looks for the the usernameAttr and privateKeyAttr in the embedded +// 'data' field within the data map. +// +// Additionaly it validates the data is in the expected KV-v2 format: +// { +// "data": {}, +// "metadata: {} +// } +// If the format does not match, it returns ("", ""). See: +// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 +func kv2Extract(sd data, usernameAttr, privateKeyAttr string) (username string, privateKey credential.PrivateKey) { + if sd == nil { + // nothing to do return early + return "", nil + } + + var data, metadata map[string]any + for k, v := range sd { + switch k { + case "data": + var ok bool + if data, ok = v.(map[string]any); !ok { + // data field should be of type map[string]any in KV-v2 + return "", nil + } + case "metadata": + var ok bool + if metadata, ok = v.(map[string]any); !ok { + // metadata field should be of type map[string]any in KV-v2 + return "", nil + } + default: + // secretData contains a non valid KV-v2 top level field + return "", nil + } + } + if data == nil || metadata == nil { + // missing required KV-v2 field + return "", nil + } + + if u, ok := data[usernameAttr]; ok { + if u, ok := u.(string); ok { + username = u + } + } + if p, ok := data[privateKeyAttr]; ok { + if p, ok := p.(string); ok { + privateKey = credential.PrivateKey(p) + } + } + + return username, privateKey +} diff --git a/internal/credential/vault/internal/sshprivatekey/sshprivatekey_test.go b/internal/credential/vault/internal/sshprivatekey/sshprivatekey_test.go new file mode 100644 index 000000000..92bb37ba8 --- /dev/null +++ b/internal/credential/vault/internal/sshprivatekey/sshprivatekey_test.go @@ -0,0 +1,274 @@ +package sshprivatekey + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/ssh/testdata" +) + +func TestExtract(t *testing.T) { + t.Parallel() + + edKey := testdata.PEMBytes["ed25519"] + rsaKey := testdata.PEMBytes["rsa"] + + type args struct { + s data + uAttr string + pAttr string + } + type sshpk struct { + user string + privateKey credential.PrivateKey + } + tests := []struct { + name string + given args + want sshpk + }{ + { + name: "nil-input", + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "no-input", + given: args{}, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "no-secret", + given: args{ + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "no-match-username-secret", + given: args{ + s: data{ + "username-wrong": "user", + "private_key": string(edKey), + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "no-match-private-key-secret", + given: args{ + s: data{ + "username": "user", + "private_key-wrong": string(edKey), + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "valid-default", + given: args{ + s: data{ + "username": "user", + "private_key": string(edKey), + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "user", privateKey: edKey}, + }, + { + name: "valid-default-private-key-string", + given: args{ + s: data{ + "username": "user", + "private_key": string(edKey), + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "user", privateKey: edKey}, + }, + { + name: "no-match-username-secret-kv2", + given: args{ + s: data{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username-wrong": "user", + "private_key": string(edKey), + }, + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "no-match-private-key-secret-kv2", + given: args{ + s: data{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "user", + "private_key-wrong": string(edKey), + }, + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "valid-kv2", + given: args{ + s: data{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "user", + "private_key": string(edKey), + }, + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "user", privateKey: edKey}, + }, + { + name: "valid-kv2-private-key-string", + given: args{ + s: data{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "user", + "private_key": string(edKey), + }, + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "user", privateKey: edKey}, + }, + { + name: "no-metadata-kv2", + given: args{ + s: data{ + "data": map[string]interface{}{ + "username": "user", + "private_key": string(edKey), + }, + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "invalid-metadata-kv2", + given: args{ + s: data{ + "metadata": "string", + "data": map[string]interface{}{ + "username": "user", + "private_key": string(edKey), + }, + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "invalid-field-kv2", + given: args{ + s: data{ + "invalid": map[string]interface{}{}, + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "user", + "private_key": string(edKey), + }, + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "", privateKey: nil}, + }, + { + name: "valid-order-default-first", + given: args{ + s: data{ + "username": "default-user", + "private_key": string(rsaKey), + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "kv2-user", + "private_key": string(edKey), + }, + }, + uAttr: "username", + pAttr: "private_key", + }, + want: sshpk{user: "default-user", privateKey: rsaKey}, + }, + { + name: "default-user-json-pointer-pk", + given: args{ + s: data{ + "username": "default-user", + "testing": map[string]interface{}{ + "private_key": string(edKey), + }, + }, + uAttr: "username", + pAttr: "/testing/private_key", + }, + want: sshpk{user: "default-user", privateKey: edKey}, + }, + { + name: "default-pk-json-pointer-user", + given: args{ + s: data{ + "private_key": string(edKey), + "testing": map[string]interface{}{ + "special": "not-so-special", + }, + }, + uAttr: "/testing/special", + pAttr: "private_key", + }, + want: sshpk{user: "not-so-special", privateKey: edKey}, + }, + { + name: "both-json-pointer", + given: args{ + s: data{ + "first-path": map[string]interface{}{ + "deeper-path": map[string]interface{}{ + "my-special-user": "you-found-me", + }, + }, + "testing": map[string]interface{}{ + "private_key": string(edKey), + }, + }, + uAttr: "/first-path/deeper-path/my-special-user", + pAttr: "/testing/private_key", + }, + want: sshpk{user: "you-found-me", privateKey: edKey}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + user, privateKey := Extract(tt.given.s, tt.given.uAttr, tt.given.pAttr) + assert.Equal(tt.want.user, user) + assert.Equal(tt.want.privateKey, privateKey) + }) + } +} diff --git a/internal/credential/vault/internal/usernamepassword/usernamepassword.go b/internal/credential/vault/internal/usernamepassword/usernamepassword.go index 6a3f66cd1..279a0157c 100644 --- a/internal/credential/vault/internal/usernamepassword/usernamepassword.go +++ b/internal/credential/vault/internal/usernamepassword/usernamepassword.go @@ -1,5 +1,11 @@ package usernamepassword +import ( + "strings" + + "github.com/mitchellh/pointerstructure" +) + type ( data map[string]interface{} @@ -31,15 +37,42 @@ func Extract(d data, usernameAttr, passwordAttr string) (string, string) { // defaultExtract looks for the usernameAttr and passwordAttr in the data map func defaultExtract(sd data, usernameAttr, passwordAttr string) (username string, password string) { - if u, ok := sd[usernameAttr]; ok { - if u, ok := u.(string); ok { - username = u - } + if sd == nil { + // nothing to do return early + return "", "" } - if p, ok := sd[passwordAttr]; ok { - if p, ok := p.(string); ok { - password = p + + var u any + switch { + case strings.HasPrefix(usernameAttr, "/"): + var err error + u, err = pointerstructure.Get(sd, usernameAttr) + if err != nil { + return "", "" } + + default: + u = sd[usernameAttr] + } + if u, ok := u.(string); ok { + username = u + } + + var p any + switch { + case strings.HasPrefix(passwordAttr, "/"): + var err error + p, err = pointerstructure.Get(sd, passwordAttr) + if err != nil { + return "", "" + } + + default: + p = sd[passwordAttr] + } + + if p, ok := p.(string); ok { + password = p } return username, password @@ -55,9 +88,14 @@ func defaultExtract(sd data, usernameAttr, passwordAttr string) (username string // } // If the format does not match, it returns ("", ""). See: // https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 -func kv2Extract(d data, usernameAttr, passwordAttr string) (username string, password string) { +func kv2Extract(sd data, usernameAttr, passwordAttr string) (username string, password string) { + if sd == nil { + // nothing to do return early + return "", "" + } + var data, metadata map[string]interface{} - for k, v := range d { + for k, v := range sd { switch k { case "data": var ok bool diff --git a/internal/credential/vault/internal/usernamepassword/usernamepassword_test.go b/internal/credential/vault/internal/usernamepassword/usernamepassword_test.go index 809e99c86..582308c3f 100644 --- a/internal/credential/vault/internal/usernamepassword/usernamepassword_test.go +++ b/internal/credential/vault/internal/usernamepassword/usernamepassword_test.go @@ -183,6 +183,52 @@ func TestBaseToUsrPass(t *testing.T) { }, want: usrPass{user: "default-user", pass: "default-pass"}, }, + { + name: "default-user-json-pointer-password", + given: args{ + s: data{ + "username": "default-user", + "testing": map[string]interface{}{ + "my-password": "secret", + }, + }, + uAttr: "username", + pAttr: "/testing/my-password", + }, + want: usrPass{user: "default-user", pass: "secret"}, + }, + { + name: "default-pk-json-pointer-user", + given: args{ + s: data{ + "password": "default-pass", + "testing": map[string]interface{}{ + "a-user-name": "me", + }, + }, + uAttr: "/testing/a-user-name", + pAttr: "password", + }, + want: usrPass{user: "me", pass: "default-pass"}, + }, + { + name: "both-json-pointer", + given: args{ + s: data{ + "first-path": map[string]interface{}{ + "deeper-path": map[string]interface{}{ + "my-special-user": "you-found-me", + }, + }, + "testing": map[string]interface{}{ + "password": "secret", + }, + }, + uAttr: "/first-path/deeper-path/my-special-user", + pAttr: "/testing/password", + }, + want: usrPass{user: "you-found-me", pass: "secret"}, + }, } for _, tt := range tests { tt := tt diff --git a/internal/credential/vault/mapping_overriders.go b/internal/credential/vault/mapping_overriders.go index 929a355a6..fbc5633ba 100644 --- a/internal/credential/vault/mapping_overriders.go +++ b/internal/credential/vault/mapping_overriders.go @@ -16,6 +16,8 @@ func validMappingOverride(m MappingOverride, ct credential.Type) bool { return true // it is always valid to not specify a mapping override case *UsernamePasswordOverride: return ct == credential.UsernamePasswordType + case *SshPrivateKeyOverride: + return ct == credential.SshPrivateKeyType default: return false // an unknown mapping override type is never valid } @@ -96,3 +98,66 @@ func (o *UsernamePasswordOverride) TableName() string { func (o *UsernamePasswordOverride) SetTableName(n string) { o.tableName = n } + +// A SshPrivateKeyOverride contains optional values for overriding the +// default mappings used to map a Vault secret to a SshPrivateKey credential +// type for the credential library that owns it. +type SshPrivateKeyOverride struct { + *store.SshPrivateKeyOverride + tableName string `gorm:"-"` +} + +var _ MappingOverride = (*SshPrivateKeyOverride)(nil) + +// NewSshPrivateKeyOverride creates a new in memory SshPrivateKeyOverride. +// WithOverrideUsernameAttribute and WithOverridePasswordAttribute are the +// only valid options. All other options are ignored. +func NewSshPrivateKeyOverride(opt ...Option) *SshPrivateKeyOverride { + opts := getOpts(opt...) + o := &SshPrivateKeyOverride{ + SshPrivateKeyOverride: &store.SshPrivateKeyOverride{ + UsernameAttribute: sanitize.String(opts.withOverrideUsernameAttribute), + PrivateKeyAttribute: sanitize.String(opts.withOverridePrivateKeyAttribute), + }, + } + return o +} + +func allocSshPrivateKeyOverride() *SshPrivateKeyOverride { + return &SshPrivateKeyOverride{ + SshPrivateKeyOverride: &store.SshPrivateKeyOverride{}, + } +} + +func (o *SshPrivateKeyOverride) clone() MappingOverride { + cp := proto.Clone(o.SshPrivateKeyOverride) + return &SshPrivateKeyOverride{ + SshPrivateKeyOverride: cp.(*store.SshPrivateKeyOverride), + } +} + +func (o *SshPrivateKeyOverride) setLibraryId(i string) { + o.LibraryId = i +} + +func (o *SshPrivateKeyOverride) sanitize() { + if sentinel.Is(o.UsernameAttribute) { + o.UsernameAttribute = "" + } + if sentinel.Is(o.PrivateKeyAttribute) { + o.PrivateKeyAttribute = "" + } +} + +// TableName returns the table name. +func (o *SshPrivateKeyOverride) TableName() string { + if o.tableName != "" { + return o.tableName + } + return "credential_vault_library_ssh_private_key_mapping_override" +} + +// SetTableName sets the table name. +func (o *SshPrivateKeyOverride) SetTableName(n string) { + o.tableName = n +} diff --git a/internal/credential/vault/mapping_overriders_test.go b/internal/credential/vault/mapping_overriders_test.go index f2bf2ac55..053d9e63a 100644 --- a/internal/credential/vault/mapping_overriders_test.go +++ b/internal/credential/vault/mapping_overriders_test.go @@ -57,6 +57,16 @@ func TestValidMappingOverrides(t *testing.T) { ct: credential.UsernamePasswordType, want: true, }, + { + m: allocSshPrivateKeyOverride(), + ct: credential.UnspecifiedType, + want: false, + }, + { + m: allocSshPrivateKeyOverride(), + ct: credential.SshPrivateKeyType, + want: true, + }, } for _, tt := range tests { tt := tt diff --git a/internal/credential/vault/options.go b/internal/credential/vault/options.go index 015dea7ba..9c57ac859 100644 --- a/internal/credential/vault/options.go +++ b/internal/credential/vault/options.go @@ -28,9 +28,10 @@ type options struct { withRequestBody []byte withCredentialType credential.Type - withOverrideUsernameAttribute string - withOverridePasswordAttribute string - withMappingOverride MappingOverride + withOverrideUsernameAttribute string + withOverridePasswordAttribute string + withOverridePrivateKeyAttribute string + withMappingOverride MappingOverride } func getDefaultOptions() options { @@ -141,6 +142,14 @@ func WithOverridePasswordAttribute(s string) Option { } } +// WithOverridePrivateKeyAttribute provides the name of an attribute in the +// Data field of a Vault api.Secret that maps to a private key value. +func WithOverridePrivateKeyAttribute(s string) Option { + return func(o *options) { + o.withOverridePrivateKeyAttribute = s + } +} + // WithMappingOverride provides an optional mapping override to use for // mapping the Data fields of a Vault api.Secret to a credential. func WithMappingOverride(m MappingOverride) Option { diff --git a/internal/credential/vault/options_test.go b/internal/credential/vault/options_test.go index 759abb793..b2a13e48b 100644 --- a/internal/credential/vault/options_test.go +++ b/internal/credential/vault/options_test.go @@ -102,6 +102,12 @@ func Test_GetOpts(t *testing.T) { testOpts.withOverridePasswordAttribute = "test" assert.Equal(t, opts, testOpts) }) + t.Run("WithOverridePrivateKeyAttribute", func(t *testing.T) { + opts := getOpts(WithOverridePrivateKeyAttribute("test")) + testOpts := getDefaultOptions() + testOpts.withOverridePrivateKeyAttribute = "test" + assert.Equal(t, opts, testOpts) + }) t.Run("WithMappingOverride", func(t *testing.T) { opts := getOpts(WithMappingOverride(unknownMapper(1))) testOpts := getDefaultOptions() diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index cb361a410..f5a5b0594 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -8,6 +8,7 @@ import ( "time" "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/timestamp" "github.com/hashicorp/boundary/internal/errors" @@ -18,17 +19,6 @@ import ( "google.golang.org/protobuf/proto" ) -var _ credential.UsernamePassword = (*usrPassCred)(nil) - -type usrPassCred struct { - *baseCred - username string - password credential.Password -} - -func (c *usrPassCred) Username() string { return c.username } -func (c *usrPassCred) Password() credential.Password { return c.password } - var _ credential.Dynamic = (*baseCred)(nil) type baseCred struct { @@ -49,10 +39,23 @@ func convert(ctx context.Context, bc *baseCred) (dynamicCred, error) { switch bc.Library().CredentialType() { case credential.UsernamePasswordType: return baseToUsrPass(ctx, bc) + case credential.SshPrivateKeyType: + return baseToSshPriKey(ctx, bc) } return bc, nil } +var _ credential.UsernamePassword = (*usrPassCred)(nil) + +type usrPassCred struct { + *baseCred + username string + password credential.Password +} + +func (c *usrPassCred) Username() string { return c.username } +func (c *usrPassCred) Password() credential.Password { return c.password } + func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) { switch { case bc == nil: @@ -82,73 +85,115 @@ func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) { }, nil } +var _ credential.SshPrivateKey = (*sshPrivateKeyCred)(nil) + +type sshPrivateKeyCred struct { + *baseCred + username string + privateKey credential.PrivateKey +} + +func (c *sshPrivateKeyCred) Username() string { return c.username } +func (c *sshPrivateKeyCred) PrivateKey() credential.PrivateKey { return c.privateKey } + +func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, error) { + switch { + case bc == nil: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred")) + case bc.lib == nil: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred.lib")) + case bc.Library().CredentialType() != credential.SshPrivateKeyType: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type")) + } + + uAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PrivateKeyAttribute + if uAttr == "" { + uAttr = "username" + } + if pAttr == "" { + pAttr = "private_key" + } + username, pk := sshprivatekey.Extract(bc.secretData, uAttr, pAttr) + if username == "" || pk == nil { + return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping)) + } + + return &sshPrivateKeyCred{ + baseCred: bc, + username: username, + privateKey: pk, + }, nil +} + var _ credential.Library = (*privateLibrary)(nil) // A privateLibrary contains all the values needed to connect to Vault and // retrieve credentials. type privateLibrary struct { - PublicId string `gorm:"primary_key"` - StoreId string - CredType string `gorm:"column:credential_type"` - UsernameAttribute string - PasswordAttribute string - Name string - Description string - CreateTime *timestamp.Timestamp - UpdateTime *timestamp.Timestamp - Version uint32 - ScopeId string - VaultPath string - HttpMethod string - HttpRequestBody []byte - VaultAddress string - Namespace string - CaCert []byte - TlsServerName string - TlsSkipVerify bool - TokenHmac []byte - Token TokenSecret - CtToken []byte - TokenKeyId string - ClientCert []byte - ClientKey KeySecret - CtClientKey []byte - ClientKeyId string - Purpose credential.Purpose `gorm:"-"` + PublicId string `gorm:"primary_key"` + StoreId string + CredType string `gorm:"column:credential_type"` + UsernameAttribute string + PasswordAttribute string + PrivateKeyAttribute string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + ScopeId string + VaultPath string + HttpMethod string + HttpRequestBody []byte + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + TokenHmac []byte + Token TokenSecret + CtToken []byte + TokenKeyId string + ClientCert []byte + ClientKey KeySecret + CtClientKey []byte + ClientKeyId string + Purpose credential.Purpose `gorm:"-"` } func (pl *privateLibrary) clone() *privateLibrary { // The 'append(a[:0:0], a...)' comes from // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F return &privateLibrary{ - PublicId: pl.PublicId, - StoreId: pl.StoreId, - CredType: pl.CredType, - UsernameAttribute: pl.UsernameAttribute, - PasswordAttribute: pl.PasswordAttribute, - Name: pl.Name, - Description: pl.Description, - CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), - UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), - Version: pl.Version, - ScopeId: pl.ScopeId, - VaultPath: pl.VaultPath, - HttpMethod: pl.HttpMethod, - HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...), - VaultAddress: pl.VaultAddress, - Namespace: pl.Namespace, - CaCert: append(pl.CaCert[:0:0], pl.CaCert...), - TlsServerName: pl.TlsServerName, - TlsSkipVerify: pl.TlsSkipVerify, - TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...), - Token: append(pl.Token[:0:0], pl.Token...), - CtToken: append(pl.CtToken[:0:0], pl.CtToken...), - TokenKeyId: pl.TokenKeyId, - ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...), - ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...), - CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), - ClientKeyId: pl.ClientKeyId, - Purpose: pl.Purpose, + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + PrivateKeyAttribute: pl.PrivateKeyAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), + UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), + Version: pl.Version, + ScopeId: pl.ScopeId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...), + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: append(pl.CaCert[:0:0], pl.CaCert...), + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...), + Token: append(pl.Token[:0:0], pl.Token...), + CtToken: append(pl.CtToken[:0:0], pl.CtToken...), + TokenKeyId: pl.TokenKeyId, + ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...), + ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...), + CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, } } diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index c54357032..4b3fe71e5 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -179,6 +179,72 @@ func TestRepository_getPrivateLibraries(t *testing.T) { req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} requests = append(requests, req) } + { + opts := []Option{ + WithCredentialType(credential.SshPrivateKeyType), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.SshPrivateKeyType), + WithMappingOverride(NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("test-username"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.SshPrivateKeyType), + WithMappingOverride(NewSshPrivateKeyOverride( + WithOverridePrivateKeyAttribute("test-private-key"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(credential.SshPrivateKeyType), + WithMappingOverride(NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("test-username"), + WithOverridePrivateKeyAttribute("test-private-key"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose} + requests = append(requests, req) + } gotLibs, err := repo.getPrivateLibraries(ctx, requests) assert.NoError(err) @@ -204,6 +270,9 @@ func TestRepository_getPrivateLibraries(t *testing.T) { case *UsernamePasswordOverride: assert.Equal(w.UsernameAttribute, got.UsernameAttribute) assert.Equal(w.PasswordAttribute, got.PasswordAttribute) + case *SshPrivateKeyOverride: + assert.Equal(w.UsernameAttribute, got.UsernameAttribute) + assert.Equal(w.PrivateKeyAttribute, got.PrivateKeyAttribute) default: assert.Fail("unknown mapping override type") } @@ -669,3 +738,368 @@ func TestBaseToUsrPass(t *testing.T) { }) } } + +func TestBaseToSshPriKey(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + given *baseCred + want *sshPrivateKeyCred + wantErr errors.Code + }{ + { + name: "nil-input", + wantErr: errors.InvalidParameter, + }, + { + name: "nil-library", + given: &baseCred{}, + wantErr: errors.InvalidParameter, + }, + { + name: "library-not-ssh-private-key-type", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.UnspecifiedType), + }, + }, + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-no-username-default-pk-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "private_key": "my-pk", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-no-pk-default-username-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "username": "my-username", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "valid-default-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "username": "my-username", + "private_key": "my-pk", + }, + }, + want: &sshPrivateKeyCred{ + username: "my-username", + privateKey: credential.PrivateKey("my-pk"), + }, + }, + { + name: "valid-override-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + UsernameAttribute: "test-username", + PrivateKeyAttribute: "test-pk", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + "test-username": "override-username", + "test-pk": "override-pk", + }, + }, + want: &sshPrivateKeyCred{ + username: "override-username", + privateKey: credential.PrivateKey("override-pk"), + }, + }, + { + name: "valid-default-username-override-pk", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + PrivateKeyAttribute: "test-pk", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + "test-username": "override-username", + "test-pk": "override-pk", + }, + }, + want: &sshPrivateKeyCred{ + username: "default-username", + privateKey: credential.PrivateKey("override-pk"), + }, + }, + { + name: "valid-override-username-default-pk", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + UsernameAttribute: "test-username", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + "test-username": "override-username", + "test-pk": "override-pk", + }, + }, + want: &sshPrivateKeyCred{ + username: "override-username", + privateKey: credential.PrivateKey("default-pk"), + }, + }, + { + name: "invalid-username-override", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + UsernameAttribute: "missing-username", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + "test-username": "override-username", + "test-pk": "override-pk", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-pk-override", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + UsernameAttribute: "missing-pk", + }, + secretData: map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + "test-username": "override-username", + "test-pk": "override-pk", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-metadata-field", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "data": map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-data-field", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-username-default-pk-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "private_key": "default-pk", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-pk-default-username-attribute", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-invalid-metadata-type", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "metadata": "hello", + "data": map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-invalid-data-type", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": "hello", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-additional-field", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "bad-field": "hello", + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "valid-kv2-default-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + }, + }, + }, + want: &sshPrivateKeyCred{ + username: "default-username", + privateKey: credential.PrivateKey("default-pk"), + }, + }, + { + name: "valid-kv2-override-attributes", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + UsernameAttribute: "test-username", + PrivateKeyAttribute: "test-pk", + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + "test-username": "override-username", + "test-pk": "override-pk", + }, + }, + }, + want: &sshPrivateKeyCred{ + username: "override-username", + privateKey: credential.PrivateKey("override-pk"), + }, + }, + { + name: "valid-kv2-default-username-override-pk", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + PrivateKeyAttribute: "test-pk", + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + "test-username": "override-username", + "test-pk": "override-pk", + }, + }, + }, + want: &sshPrivateKeyCred{ + username: "default-username", + privateKey: credential.PrivateKey("override-pk"), + }, + }, + { + name: "valid-kv2-override-username-default-pk", + given: &baseCred{ + lib: &privateLibrary{ + CredType: string(credential.SshPrivateKeyType), + UsernameAttribute: "test-username", + }, + secretData: map[string]interface{}{ + "metadata": map[string]interface{}{}, + "data": map[string]interface{}{ + "username": "default-username", + "private_key": "default-pk", + "test-username": "override-username", + "test-pk": "override-pk", + }, + }, + }, + want: &sshPrivateKeyCred{ + username: "override-username", + privateKey: credential.PrivateKey("default-pk"), + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := baseToSshPriKey(context.Background(), tt.given) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + want := tt.want + want.baseCred = tt.given + assert.Equal(want, got) + }) + } +} diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index f16a6dfdf..5c405a83d 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db" dbcommon "github.com/hashicorp/boundary/internal/db/common" "github.com/hashicorp/boundary/internal/db/timestamp" @@ -344,19 +345,20 @@ func (r *Repository) LookupCredentialLibrary(ctx context.Context, publicId strin // mapping overrides. It does not include encrypted data and is safe to // return external to boundary. type publicLibrary 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 - CredentialType string - UsernameAttribute string - PasswordAttribute string + 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 + CredentialType string + UsernameAttribute string + PasswordAttribute string + PrivateKeyAttribute string } func allocPublicLibrary() *publicLibrary { @@ -377,13 +379,25 @@ func (pl *publicLibrary) toCredentialLibrary() *CredentialLibrary { cl.HttpRequestBody = pl.HttpRequestBody cl.CredentialLibrary.CredentialType = pl.CredentialType - if pl.UsernameAttribute != "" || pl.PasswordAttribute != "" { - up := allocUsernamePasswordOverride() - up.LibraryId = pl.PublicId - up.UsernameAttribute = pl.UsernameAttribute - up.PasswordAttribute = pl.PasswordAttribute - up.sanitize() - cl.MappingOverride = up + switch pl.CredentialType { + case string(credential.UsernamePasswordType): + if pl.UsernameAttribute != "" || pl.PasswordAttribute != "" { + up := allocUsernamePasswordOverride() + up.LibraryId = pl.PublicId + up.UsernameAttribute = pl.UsernameAttribute + up.PasswordAttribute = pl.PasswordAttribute + up.sanitize() + cl.MappingOverride = up + } + case string(credential.SshPrivateKeyType): + if pl.UsernameAttribute != "" || pl.PrivateKeyAttribute != "" { + pk := allocSshPrivateKeyOverride() + pk.LibraryId = pl.PublicId + pk.UsernameAttribute = pl.UsernameAttribute + pk.PrivateKeyAttribute = pl.PrivateKeyAttribute + pk.sanitize() + cl.MappingOverride = pk + } } return cl } diff --git a/internal/credential/vault/repository_credential_library_test.go b/internal/credential/vault/repository_credential_library_test.go index f383a6ce5..54bfd0bb5 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -285,6 +285,128 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { }, }, }, + { + name: "valid-ssh-private-key-credential-type", + in: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + want: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, + { + name: "unknown-ssh-private-key-mapping-override-type", + in: &CredentialLibrary{ + MappingOverride: unknownMapper(1), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + wantErr: errors.VaultInvalidMappingOverride, + }, + { + name: "invalid-ssh-private-key-mapping-override-type", + in: &CredentialLibrary{ + MappingOverride: NewUsernamePasswordOverride(WithOverrideUsernameAttribute("test")), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + wantErr: errors.VaultInvalidMappingOverride, + }, + { + name: "valid-ssh-prviate-key-credential-type-with-username-override", + in: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("utest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("utest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, + { + name: "valid-ssh-private-key-credential-type-with-private-key-override", + in: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverridePrivateKeyAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverridePrivateKeyAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, + { + name: "valid-ssh-private-keyt-credential-type-with-username-and-private-key-override", + in: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("utest"), + WithOverridePrivateKeyAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("utest"), + WithOverridePrivateKeyAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, } for _, tt := range tests { @@ -324,13 +446,24 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { require.True(ok) assert.Equal(w.UsernameAttribute, g.UsernameAttribute) assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + + // verify it was persisted in the database + override := allocUsernamePasswordOverride() + assert.NoError(rw.LookupWhere(ctx, &override, "library_id = ?", []interface{}{got.GetPublicId()})) + + case *SshPrivateKeyOverride: + g, ok := got.MappingOverride.(*SshPrivateKeyOverride) + require.True(ok) + assert.Equal(w.UsernameAttribute, g.UsernameAttribute) + assert.Equal(w.PrivateKeyAttribute, g.PrivateKeyAttribute) + + // verify it was persisted in the database + override := allocSshPrivateKeyOverride() + assert.NoError(rw.LookupWhere(ctx, &override, "library_id = ?", []interface{}{got.GetPublicId()})) + default: assert.Fail("Unknown mapping override") } - - // verify it was persisted in the database - override := allocUsernamePasswordOverride() - assert.NoError(rw.LookupWhere(ctx, &override, "library_id = ?", []interface{}{got.GetPublicId()})) } }) } @@ -1104,6 +1237,162 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { masks: []string{"MappingOverride"}, wantErr: errors.VaultInvalidMappingOverride, }, + { + name: "ssh-private-key-attributes-change-username-attribute", + orig: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("orig-username"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + chgFn: changeMappingOverride( + NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("changed-username"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("changed-username"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + wantCount: 1, + }, + { + name: "ssh-private-key-attributes-change-private-key-attribute", + orig: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverridePrivateKeyAttribute("orig-private-key"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + chgFn: changeMappingOverride( + NewSshPrivateKeyOverride( + WithOverridePrivateKeyAttribute("changed-private-key"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverridePrivateKeyAttribute("changed-private-key"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + wantCount: 1, + }, + { + name: "ssh-private-key-attributes-change-username-and-private-key-attributes", + orig: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("orig-username"), + WithOverridePrivateKeyAttribute("orig-private-key"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + chgFn: changeMappingOverride( + NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePrivateKeyAttribute("changed-private-key"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePrivateKeyAttribute("changed-private-key"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + wantCount: 1, + }, + { + name: "no-mapping-override-change-username-and-private-key-attributes", + orig: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + chgFn: changeMappingOverride( + NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePrivateKeyAttribute("changed-private-key"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("changed-username"), + WithOverridePrivateKeyAttribute("changed-private-key"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + wantCount: 1, + }, + { + name: "ssh-private-key-attributes-delete-mapping-override", + orig: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("orig-username"), + WithOverridePrivateKeyAttribute("orig-private-key"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + chgFn: changeMappingOverride(nil), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-ssh-private-key-repo", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + wantCount: 1, + }, } for _, tt := range tests { @@ -1172,6 +1461,11 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { require.True(ok) assert.Equal(w.UsernameAttribute, g.UsernameAttribute) assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + case *SshPrivateKeyOverride: + g, ok := got.MappingOverride.(*SshPrivateKeyOverride) + require.True(ok) + assert.Equal(w.UsernameAttribute, g.UsernameAttribute) + assert.Equal(w.PrivateKeyAttribute, g.PrivateKeyAttribute) default: assert.Fail("Unknown mapping override") } @@ -1371,6 +1665,60 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) { }, }, }, + { + name: "valid-ssh-private-key-credential-type", + in: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, + { + name: "valid-ssh-private-key-credential-type-with-username-override", + in: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("utest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, + { + name: "valid-ssh-private-key-credential-type-with-private-key-override", + in: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverridePrivateKeyAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, + { + name: "valid-ssh-private-key-credential-type-with-username-and-private-key-override", + in: &CredentialLibrary{ + MappingOverride: NewSshPrivateKeyOverride( + WithOverrideUsernameAttribute("utest"), + WithOverridePrivateKeyAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(credential.SshPrivateKeyType), + }, + }, + }, } for _, tt := range tests { @@ -1403,6 +1751,11 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) { require.True(ok) assert.Equal(w.UsernameAttribute, g.UsernameAttribute) assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + case *SshPrivateKeyOverride: + g, ok := got.MappingOverride.(*SshPrivateKeyOverride) + require.True(ok) + assert.Equal(w.UsernameAttribute, g.UsernameAttribute) + assert.Equal(w.PrivateKeyAttribute, g.PrivateKeyAttribute) default: assert.Fail("Unknown mapping override") } diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index a86a6b068..a9447fafa 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -46,7 +46,10 @@ func TestRepository_IssueCredentials(t *testing.T) { _, token := v.CreateToken(t, vault.WithPolicies([]string{"default", "boundary-controller", "database", "pki", "secret"})) // Create valid username password KV secret - v.CreateKVSecret(t, "my-secret", []byte(`{"data":{"username":"user","password":"pass"}}`)) + v.CreateKVSecret(t, "my-up-secret", []byte(`{"data":{"username":"user","password":"pass"}}`)) + + // Create valid ssh private key KV secret + v.CreateKVSecret(t, "my-sshpk-secret", []byte(`{"data":{"username":"user","private_key":"pk data"}}`)) var opts []vault.Option opts = append(opts, vault.WithCACert(v.CaCert)) @@ -71,6 +74,7 @@ func TestRepository_IssueCredentials(t *testing.T) { libKV libErrKV libUsrPassKV + libSshPkKV ) libs := make(map[libT]string) @@ -106,7 +110,7 @@ func TestRepository_IssueCredentials(t *testing.T) { libs[libErrPKI] = lib.GetPublicId() } { - libPath := path.Join("secret", "data", "my-secret") + libPath := path.Join("secret", "data", "my-up-secret") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) assert.NoError(t, err) require.NotNil(t, libIn) @@ -156,7 +160,7 @@ func TestRepository_IssueCredentials(t *testing.T) { libs[libErrUsrPassDB] = lib.GetPublicId() } { - libPath := path.Join("secret", "data", "my-secret") + libPath := path.Join("secret", "data", "my-up-secret") opts := []vault.Option{ vault.WithCredentialType(credential.UsernamePasswordType), } @@ -168,6 +172,19 @@ func TestRepository_IssueCredentials(t *testing.T) { require.NotNil(t, lib) libs[libUsrPassKV] = lib.GetPublicId() } + { + libPath := path.Join("secret", "data", "my-sshpk-secret") + opts := []vault.Option{ + vault.WithCredentialType(credential.SshPrivateKeyType), + } + libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...) + assert.NoError(t, err) + require.NotNil(t, libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(t, err) + require.NotNil(t, lib) + libs[libSshPkKV] = lib.GetPublicId() + } at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) uId := at.GetIamUserId() @@ -300,6 +317,16 @@ func TestRepository_IssueCredentials(t *testing.T) { }, }, }, + { + name: "valid-kv-ssh-private-key-library", + convertFn: rc2dc, + requests: []credential.Request{ + { + SourceId: libs[libSshPkKV], + Purpose: credential.ApplicationPurpose, + }, + }, + }, } for _, tt := range tests { tt := tt diff --git a/internal/credential/vault/store/vault.pb.go b/internal/credential/vault/store/vault.pb.go index d54271882..d327687f3 100644 --- a/internal/credential/vault/store/vault.pb.go +++ b/internal/credential/vault/store/vault.pb.go @@ -868,6 +868,87 @@ func (x *UsernamePasswordOverride) GetPasswordAttribute() string { return "" } +type SshPrivateKeyOverride struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // library_id of the owning vault credential library. + // @inject_tag: `gorm:"primary_key"` + LibraryId string `protobuf:"bytes,1,opt,name=library_id,json=libraryId,proto3" json:"library_id,omitempty" gorm:"primary_key"` + // username_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a username. + // If set, it overrides any default attribute names the system uses to + // find a username attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + UsernameAttribute string `protobuf:"bytes,2,opt,name=username_attribute,json=usernameAttribute,proto3" json:"username_attribute,omitempty" gorm:"default:null"` + // private_key_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a private key. + // If set, it overrides any default attribute names the system uses to + // find a private_key attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + PrivateKeyAttribute string `protobuf:"bytes,3,opt,name=private_key_attribute,json=privateKeyAttribute,proto3" json:"private_key_attribute,omitempty" gorm:"default:null"` +} + +func (x *SshPrivateKeyOverride) Reset() { + *x = SshPrivateKeyOverride{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SshPrivateKeyOverride) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshPrivateKeyOverride) ProtoMessage() {} + +func (x *SshPrivateKeyOverride) ProtoReflect() protoreflect.Message { + 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 { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SshPrivateKeyOverride.ProtoReflect.Descriptor instead. +func (*SshPrivateKeyOverride) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{6} +} + +func (x *SshPrivateKeyOverride) GetLibraryId() string { + if x != nil { + return x.LibraryId + } + return "" +} + +func (x *SshPrivateKeyOverride) GetUsernameAttribute() string { + if x != nil { + return x.UsernameAttribute + } + return "" +} + +func (x *SshPrivateKeyOverride) GetPrivateKeyAttribute() string { + if x != nil { + return x.PrivateKeyAttribute + } + return "" +} + var File_controller_storage_credential_vault_store_v1_vault_proto protoreflect.FileDescriptor var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []byte{ @@ -1075,12 +1156,21 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []by 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, 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, + 0x74, 0x65, 0x22, 0x99, 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, 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 ( @@ -1095,7 +1185,7 @@ 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, 6) +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_goTypes = []interface{}{ (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token @@ -1103,22 +1193,23 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_goTypes = []in (*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 - (*timestamp.Timestamp)(nil), // 6: controller.storage.timestamp.v1.Timestamp + (*SshPrivateKeyOverride)(nil), // 6: controller.storage.credential.vault.store.v1.SshPrivateKeyOverride + (*timestamp.Timestamp)(nil), // 7: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_vault_store_v1_vault_proto_depIdxs = []int32{ - 6, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 9: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 10: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 11: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 12: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 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 @@ -1204,6 +1295,18 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { return nil } } + file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SshPrivateKeyOverride); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1211,7 +1314,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: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go index d697dc0df..05eb597c5 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -38,8 +38,9 @@ const ( // Credential mapping override attributes const ( - usernameAttribute string = "username_attribute" - passwordAttribute string = "password_attribute" + usernameAttribute string = "username_attribute" + passwordAttribute string = "password_attribute" + privateKeyAttribute string = "private_key_attribute" ) var ( @@ -531,6 +532,14 @@ func toProto(in credential.Library, opt ...handlers.Option) (*pb.CredentialLibra if mapping.PasswordAttribute != "" { m[passwordAttribute] = mapping.PasswordAttribute } + + case *vault.SshPrivateKeyOverride: + if mapping.UsernameAttribute != "" { + m[usernameAttribute] = mapping.UsernameAttribute + } + if mapping.PrivateKeyAttribute != "" { + m[privateKeyAttribute] = mapping.PrivateKeyAttribute + } } if len(m) > 0 { mp, err := structpb.NewStruct(m) @@ -592,6 +601,20 @@ func toStorageVaultLibrary(storeId string, in *pb.CredentialLibrary) (out *vault if len(mapOpts) > 0 { opts = append(opts, vault.WithMappingOverride(vault.NewUsernamePasswordOverride(mapOpts...))) } + + case credential.SshPrivateKeyType: + opts = append(opts, vault.WithCredentialType(credentialType)) + overrides := in.CredentialMappingOverrides.AsMap() + var mapOpts []vault.Option + if username := overrides[usernameAttribute]; username != nil { + mapOpts = append(mapOpts, vault.WithOverrideUsernameAttribute(username.(string))) + } + if pk := overrides[privateKeyAttribute]; pk != nil { + mapOpts = append(mapOpts, vault.WithOverridePrivateKeyAttribute(pk.(string))) + } + if len(mapOpts) > 0 { + opts = append(opts, vault.WithMappingOverride(vault.NewSshPrivateKeyOverride(mapOpts...))) + } } cs, err := vault.NewCredentialLibrary(storeId, attrs.GetPath().GetValue(), opts...) @@ -697,6 +720,9 @@ func validateMapping(badFields map[string]string, credentialType credential.Type case credential.UsernamePasswordType: validFields[usernameAttribute] = true validFields[passwordAttribute] = true + case credential.SshPrivateKeyType: + validFields[usernameAttribute] = true + validFields[privateKeyAttribute] = true default: badFields[globals.CredentialTypeField] = fmt.Sprintf("Unknown credential type %q", credentialType) return @@ -754,6 +780,27 @@ func getMappingUpdates(credentialType credential.Type, current vault.MappingOver default: ret[passwordAttribute] = currentPass } + + case credential.SshPrivateKeyType: + var currentUser, currentPk interface{} + if overrides, ok := current.(*vault.SshPrivateKeyOverride); ok { + currentUser = overrides.UsernameAttribute + currentPk = overrides.PrivateKeyAttribute + } + + switch { + case masks[usernameAttribute]: + ret[usernameAttribute] = new[usernameAttribute] + default: + ret[usernameAttribute] = currentUser + } + + switch { + case masks[privateKeyAttribute]: + ret[privateKeyAttribute] = new[privateKeyAttribute] + default: + ret[privateKeyAttribute] = currentPk + } } return ret, true diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index e2930f7fb..ac9d9018f 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -606,6 +606,7 @@ func TestGet(t *testing.T) { repo, err := repoFn() require.NoError(t, err) + lib, err := vault.NewCredentialLibrary(store.GetPublicId(), "vault/path", vault.WithCredentialType("username_password"), vault.WithMappingOverride( @@ -617,6 +618,17 @@ func TestGet(t *testing.T) { userPassLib, err := repo.CreateCredentialLibrary(context.Background(), prj.GetPublicId(), lib) require.NoError(t, err) + lib1, err := vault.NewCredentialLibrary(store.GetPublicId(), "vault/path/ssh", + vault.WithCredentialType("ssh_private_key"), + vault.WithMappingOverride( + vault.NewSshPrivateKeyOverride( + vault.WithOverrideUsernameAttribute("user"), + vault.WithOverridePrivateKeyAttribute("pk"), + ))) + require.NoError(t, err) + sshPkLib, err := repo.CreateCredentialLibrary(context.Background(), prj.GetPublicId(), lib1) + require.NoError(t, err) + cases := []struct { name string id string @@ -677,6 +689,38 @@ func TestGet(t *testing.T) { }, }, }, + { + name: "success-ssh-private-key", + id: sshPkLib.GetPublicId(), + res: &pbs.GetCredentialLibraryResponse{ + Item: &pb.CredentialLibrary{ + Id: sshPkLib.GetPublicId(), + CredentialStoreId: sshPkLib.GetStoreId(), + Scope: &scopepb.ScopeInfo{Id: store.GetScopeId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + Type: vault.Subtype.String(), + AuthorizedActions: testAuthorizedActions, + CreatedTime: sshPkLib.CreateTime.GetTimestamp(), + UpdatedTime: sshPkLib.UpdateTime.GetTimestamp(), + Version: 1, + Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(sshPkLib.GetVaultPath()), + HttpMethod: wrapperspb.String(sshPkLib.GetHttpMethod()), + }, + }, + CredentialType: "ssh_private_key", + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "user", + privateKeyAttribute: "pk", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", vault.CredentialLibraryPrefix), @@ -819,6 +863,7 @@ func TestUpdate(t *testing.T) { usernameAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, usernameAttribute) passwordAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, passwordAttribute) + privateKeyAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, privateKeyAttribute) successCases := []struct { name string @@ -1114,6 +1159,220 @@ func TestUpdate(t *testing.T) { return out }, }, + { + name: "ssh-private-key-attributes-change-username-attribute", + opts: []vault.Option{ + vault.WithCredentialType("ssh_private_key"), + vault.WithMappingOverride( + vault.NewSshPrivateKeyOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePrivateKeyAttribute("orig-pk"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "changed-user", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides.Fields[usernameAttribute] = structpb.NewStringValue("changed-user") + return out + }, + }, + { + name: "ssh-private-key-attributes-change-private-key-attribute", + opts: []vault.Option{ + vault.WithCredentialType("ssh_private_key"), + vault.WithMappingOverride( + vault.NewSshPrivateKeyOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePrivateKeyAttribute("orig-pk"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(privateKeyAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + privateKeyAttribute: "changed-pk", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides.Fields[privateKeyAttribute] = structpb.NewStringValue("changed-pk") + return out + }, + }, + { + name: "ssh-private-key-attributes-change-username-and-private-key-attributes", + opts: []vault.Option{ + vault.WithCredentialType("ssh_private_key"), + vault.WithMappingOverride( + vault.NewSshPrivateKeyOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePrivateKeyAttribute("orig-pk"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "changed-user", + privateKeyAttribute: "changed-pk", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides.Fields[usernameAttribute] = structpb.NewStringValue("changed-user") + out.CredentialMappingOverrides.Fields[privateKeyAttribute] = structpb.NewStringValue("changed-pk") + return out + }, + }, + { + name: "no-mapping-override-change-username-and-private-key-attributes", + opts: []vault.Option{ + vault.WithCredentialType("ssh_private_key"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: "new-user", + privateKeyAttribute: "new-pk", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + v := map[string]interface{}{ + usernameAttribute: "new-user", + privateKeyAttribute: "new-pk", + } + var err error + out.CredentialMappingOverrides, err = structpb.NewStruct(v) + require.NoError(t, err) + return out + }, + }, + { + name: "ssh-private-key-attributes-delete-mapping-override", + opts: []vault.Option{ + vault.WithCredentialType("ssh_private_key"), + vault.WithMappingOverride( + vault.NewSshPrivateKeyOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePrivateKeyAttribute("orig-pk"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(credentialMappingPathField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: nil, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, + { + name: "ssh-private-key-no-mapping-override-delete-mapping-override", + opts: []vault.Option{ + vault.WithCredentialType("ssh_private_key"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(credentialMappingPathField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: nil, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, + { + name: "ssh-private-key-attributes-delete-mapping-override-field-specific", + opts: []vault.Option{ + vault.WithCredentialType("ssh_private_key"), + vault.WithMappingOverride( + vault.NewSshPrivateKeyOverride( + vault.WithOverrideUsernameAttribute("orig-user"), + vault.WithOverridePrivateKeyAttribute("orig-pk"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: nil, + privateKeyAttribute: nil, + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, + { + name: "ssh-private-key-no-mapping-override-delete-mapping-override-field-specific", + opts: []vault.Option{ + vault.WithCredentialType("ssh_private_key"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]interface{}{ + usernameAttribute: nil, + privateKeyAttribute: nil, + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, } for _, tc := range successCases { diff --git a/internal/daemon/controller/handlers/targets/credentials.go b/internal/daemon/controller/handlers/targets/credentials.go index 11acc2be6..13ca0b3be 100644 --- a/internal/daemon/controller/handlers/targets/credentials.go +++ b/internal/daemon/controller/handlers/targets/credentials.go @@ -34,6 +34,16 @@ func dynamicToWorkerCredential(ctx context.Context, cred credential.Dynamic) (se }, } + case credential.SshPrivateKey: + workerCred = &serverpb.Credential{ + Credential: &serverpb.Credential_SshPrivateKey{ + SshPrivateKey: &serverpb.SshPrivateKey{ + Username: c.Username(), + PrivateKey: string(c.PrivateKey()), + }, + }, + } + default: return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported credential %T", c)) } @@ -90,6 +100,17 @@ func dynamicToSessionCredential(ctx context.Context, cred credential.Dynamic) (* return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for credential")) } + case credential.SshPrivateKey: + credData, err = handlers.ProtoToStruct( + &pb.SshPrivateKeyCredential{ + Username: c.Username(), + PrivateKey: string(c.PrivateKey()), + }, + ) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for credential")) + } + default: return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported credential %T", c)) } @@ -128,6 +149,16 @@ func staticToWorkerCredential(ctx context.Context, cred credential.Static) (sess }, } + case *credstatic.SshPrivateKeyCredential: + workerCred = &serverpb.Credential{ + Credential: &serverpb.Credential_SshPrivateKey{ + SshPrivateKey: &serverpb.SshPrivateKey{ + Username: c.GetUsername(), + PrivateKey: string(c.GetPrivateKey()), + }, + }, + } + default: return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported credential %T", c)) } @@ -160,7 +191,24 @@ func staticToSessionCredential(ctx context.Context, cred credential.Static) (*pb "password": string(c.GetPassword()), } if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for static credential")) + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for username password credential")) + } + + case *credstatic.SshPrivateKeyCredential: + var err error + credType = string(credential.SshPrivateKeyType) + credData, err = handlers.ProtoToStruct( + &pb.SshPrivateKeyCredential{ + Username: c.GetUsername(), + PrivateKey: string(c.GetPrivateKey()), + }, + ) + secret = map[string]interface{}{ + "username": c.GetUsername(), + "private_key": string(c.GetPrivateKey()), + } + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for ssh private key credential")) } default: @@ -172,6 +220,9 @@ func staticToSessionCredential(ctx context.Context, cred credential.Static) (*pb return nil, errors.Wrap(ctx, err, op, errors.WithMsg("marshalling static secret to json")) } sSecret, err := structpb.NewStruct(secret) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for secret")) + } return &pb.SessionCredential{ CredentialSource: &pb.CredentialSource{ diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 28ff95531..0b08996e2 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -1796,13 +1796,21 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ badFields[globals.EgressCredentialSourceIdsField] = "Application or Egress Credential Source IDs must be provided." } for _, cl := range req.GetApplicationCredentialSourceIds() { - if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) { + if !handlers.ValidId(handlers.Id(cl), + vault.CredentialLibraryPrefix, + credential.UsernamePasswordCredentialPrefix, + credential.PreviousUsernamePasswordCredentialPrefix, + credential.SshPrivateKeyCredentialPrefix) { badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } } for _, cl := range req.GetEgressCredentialSourceIds() { - if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) { + if !handlers.ValidId(handlers.Id(cl), + vault.CredentialLibraryPrefix, + credential.UsernamePasswordCredentialPrefix, + credential.PreviousUsernamePasswordCredentialPrefix, + credential.SshPrivateKeyCredentialPrefix) { badFields[globals.EgressCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } @@ -1822,13 +1830,21 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ badFields[globals.VersionField] = "Required field." } for _, cl := range req.GetApplicationCredentialSourceIds() { - if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) { + if !handlers.ValidId(handlers.Id(cl), + vault.CredentialLibraryPrefix, + credential.UsernamePasswordCredentialPrefix, + credential.PreviousUsernamePasswordCredentialPrefix, + credential.SshPrivateKeyCredentialPrefix) { badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } } for _, cl := range req.GetEgressCredentialSourceIds() { - if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) { + if !handlers.ValidId(handlers.Id(cl), + vault.CredentialLibraryPrefix, + credential.UsernamePasswordCredentialPrefix, + credential.PreviousUsernamePasswordCredentialPrefix, + credential.SshPrivateKeyCredentialPrefix) { badFields[globals.EgressCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } @@ -1852,13 +1868,21 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc badFields[globals.EgressCredentialSourceIdsField] = "Application or Egress Credential Source IDs must be provided." } for _, cl := range req.GetApplicationCredentialSourceIds() { - if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) { + if !handlers.ValidId(handlers.Id(cl), + vault.CredentialLibraryPrefix, + credential.UsernamePasswordCredentialPrefix, + credential.PreviousUsernamePasswordCredentialPrefix, + credential.SshPrivateKeyCredentialPrefix) { badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } } for _, cl := range req.GetEgressCredentialSourceIds() { - if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) { + if !handlers.ValidId(handlers.Id(cl), + vault.CredentialLibraryPrefix, + credential.UsernamePasswordCredentialPrefix, + credential.PreviousUsernamePasswordCredentialPrefix, + credential.SshPrivateKeyCredentialPrefix) { badFields[globals.EgressCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } 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 52d44c358..b2119f99e 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -43,6 +43,7 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh/testdata" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -2732,7 +2733,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { staticStore := credstatic.TestCredentialStore(t, conn, wrapper, proj.GetPublicId()) credService, err := credentials.NewService(staticCredRepoFn, iamRepoFn) require.NoError(t, err) - credResp, err := credService.CreateCredential(ctx, &pbs.CreateCredentialRequest{Item: &credpb.Credential{ + upCredResp, err := credService.CreateCredential(ctx, &pbs.CreateCredentialRequest{Item: &credpb.Credential{ CredentialStoreId: staticStore.GetPublicId(), Type: credential.UsernamePasswordSubtype.String(), Name: wrapperspb.String("Cred Name"), @@ -2745,7 +2746,62 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { }, }}) require.NoError(t, err) - require.NotNil(t, credResp) + require.NotNil(t, upCredResp) + + sshPkCredResp, err := credService.CreateCredential(ctx, &pbs.CreateCredentialRequest{Item: &credpb.Credential{ + CredentialStoreId: staticStore.GetPublicId(), + Type: credential.SshPrivateKeySubtype.String(), + Name: wrapperspb.String("Cred SSH Name"), + Description: wrapperspb.String("Cred SSH Description"), + Attrs: &credpb.Credential_SshPrivateKeyAttributes{ + SshPrivateKeyAttributes: &credpb.SshPrivateKeyAttributes{ + Username: wrapperspb.String("static-username"), + PrivateKey: wrapperspb.String(string(testdata.PEMBytes["ed25519"])), + }, + }, + }}) + require.NoError(t, err) + require.NotNil(t, sshPkCredResp) + + // Create secret in vault with default username and password fields + defaultSshPrivateKey := v.CreateKVSecret(t, "default-sshpk", []byte(`{"data": {"username": "my-user", "private_key": "my-pk"}}`)) + require.NotNil(t, defaultSshPrivateKey) + + clsRespSshPrivateKey, err := credLibService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ + CredentialStoreId: vaultStore.GetPublicId(), + Name: wrapperspb.String("SSH Private Key Library"), + Description: wrapperspb.String("SSH Private Key Library Description"), + Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(path.Join("secret", "data", "default-sshpk")), + HttpMethod: wrapperspb.String("GET"), + }, + }, + CredentialType: "ssh_private_key", + }}) + require.NoError(t, err) + + // Create secret in vault with non default username and password fields + nonDefaultSshPrivateKey := v.CreateKVSecret(t, "non-default-sshpk", []byte(`{"data": {"non-default-user": "my-user", "non-default-pk": "my-special-pk"}}`)) + require.NotNil(t, nonDefaultSshPrivateKey) + + clsRespSshPrivateKeyWithMapping, err := credLibService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ + CredentialStoreId: vaultStore.GetPublicId(), + Name: wrapperspb.String("SSH Private Key Mapping Library"), + Description: wrapperspb.String("SSH Private Key Mapping Library Description"), + Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(path.Join("secret", "data", "non-default-sshpk")), + HttpMethod: wrapperspb.String("GET"), + }, + }, + CredentialType: "ssh_private_key", + CredentialMappingOverrides: &structpb.Struct{Fields: map[string]*structpb.Value{ + "username_attribute": structpb.NewStringValue("non-default-user"), + "private_key_attribute": structpb.NewStringValue("non-default-pk"), + }}, + }}) + require.NoError(t, err) cases := []struct { name string @@ -2826,14 +2882,14 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { { name: "static-UsernamePassword", hostSourceId: shs.GetPublicId(), - credSourceId: credResp.GetItem().GetId(), + credSourceId: upCredResp.GetItem().GetId(), wantedHostId: h.GetPublicId(), wantedEndpoint: h.GetAddress(), wantedCred: &pb.SessionCredential{ CredentialSource: &pb.CredentialSource{ - Id: credResp.GetItem().GetId(), - Name: credResp.GetItem().GetName().GetValue(), - Description: credResp.GetItem().GetDescription().GetValue(), + Id: upCredResp.GetItem().GetId(), + Name: upCredResp.GetItem().GetName().GetValue(), + Description: upCredResp.GetItem().GetDescription().GetValue(), CredentialStoreId: staticStore.GetPublicId(), Type: credstatic.Subtype.String(), CredentialType: string(credential.UsernamePasswordType), @@ -2849,6 +2905,84 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { }(), }, }, + { + name: "static-ssh-private-key", + hostSourceId: shs.GetPublicId(), + credSourceId: sshPkCredResp.GetItem().GetId(), + wantedHostId: h.GetPublicId(), + wantedEndpoint: h.GetAddress(), + wantedCred: &pb.SessionCredential{ + CredentialSource: &pb.CredentialSource{ + Id: sshPkCredResp.GetItem().GetId(), + Name: sshPkCredResp.GetItem().GetName().GetValue(), + Description: sshPkCredResp.GetItem().GetDescription().GetValue(), + CredentialStoreId: staticStore.GetPublicId(), + Type: credstatic.Subtype.String(), + CredentialType: string(credential.SshPrivateKeyType), + }, + Credential: func() *structpb.Struct { + data := map[string]interface{}{ + "private_key": string(testdata.PEMBytes["ed25519"]), + "username": "static-username", + } + st, err := structpb.NewStruct(data) + require.NoError(t, err) + return st + }(), + }, + }, + { + name: "vault-ssh-private-key", + hostSourceId: shs.GetPublicId(), + credSourceId: clsRespSshPrivateKey.GetItem().GetId(), + wantedHostId: h.GetPublicId(), + wantedEndpoint: h.GetAddress(), + wantedCred: &pb.SessionCredential{ + CredentialSource: &pb.CredentialSource{ + Id: clsRespSshPrivateKey.GetItem().GetId(), + Name: clsRespSshPrivateKey.GetItem().GetName().GetValue(), + Description: clsRespSshPrivateKey.GetItem().GetDescription().GetValue(), + CredentialStoreId: vaultStore.GetPublicId(), + Type: vault.Subtype.String(), + CredentialType: string(credential.SshPrivateKeyType), + }, + Credential: func() *structpb.Struct { + data := map[string]interface{}{ + "private_key": "my-pk", + "username": "my-user", + } + st, err := structpb.NewStruct(data) + require.NoError(t, err) + return st + }(), + }, + }, + { + name: "vault-ssh-private-key-with-mapping", + hostSourceId: shs.GetPublicId(), + credSourceId: clsRespSshPrivateKeyWithMapping.GetItem().GetId(), + wantedHostId: h.GetPublicId(), + wantedEndpoint: h.GetAddress(), + wantedCred: &pb.SessionCredential{ + CredentialSource: &pb.CredentialSource{ + Id: clsRespSshPrivateKeyWithMapping.GetItem().GetId(), + Name: clsRespSshPrivateKeyWithMapping.GetItem().GetName().GetValue(), + Description: clsRespSshPrivateKeyWithMapping.GetItem().GetDescription().GetValue(), + CredentialStoreId: vaultStore.GetPublicId(), + Type: vault.Subtype.String(), + CredentialType: string(credential.SshPrivateKeyType), + }, + Credential: func() *structpb.Struct { + data := map[string]interface{}{ + "username": "my-user", + "private_key": "my-special-pk", + } + st, err := structpb.NewStruct(data) + require.NoError(t, err) + return st + }(), + }, + }, } for _, tc := range cases { diff --git a/internal/db/schema/migrations/oss/postgres/36/02_vault_private_library.up.sql b/internal/db/schema/migrations/oss/postgres/36/02_vault_private_library.up.sql index 8403cdd88..4520efa5f 100644 --- a/internal/db/schema/migrations/oss/postgres/36/02_vault_private_library.up.sql +++ b/internal/db/schema/migrations/oss/postgres/36/02_vault_private_library.up.sql @@ -1,9 +1,9 @@ begin; --- Replaces view from 22/05_vault_private_library.up.sql drop view credential_vault_library_public; drop view credential_vault_library_private; + -- Replaced in 39/02_vault_ssh_private_key_override.up.sql create view credential_vault_library_private as with password_override (library_id, username_attribute, password_attribute) as ( @@ -47,6 +47,7 @@ begin; 'credential_vault_library_private is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + -- Replaced in 39/02_vault_ssh_private_key_override.up.sql create view credential_vault_library_public as select public_id, store_id, diff --git a/internal/db/schema/migrations/oss/postgres/39/02_vault_ssh_private_key_override.up.sql b/internal/db/schema/migrations/oss/postgres/39/02_vault_ssh_private_key_override.up.sql new file mode 100644 index 000000000..7a054bdcf --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/39/02_vault_ssh_private_key_override.up.sql @@ -0,0 +1,114 @@ +begin; + + -- add ssh_private_key type + insert into credential_type_enm (name) + values ('ssh_private_key'); + + create table credential_vault_library_ssh_private_key_mapping_override ( + library_id wt_public_id primary key + constraint credential_vault_library_fkey + references credential_vault_library (public_id) + on delete cascade + on update cascade + constraint credential_vault_library_mapping_override_fkey + references credential_vault_library_mapping_override (library_id) + on delete cascade + on update cascade, + username_attribute wt_sentinel + default wt_to_sentinel('no override') + not null, + private_key_attribute wt_sentinel + default wt_to_sentinel('no override') + not null + ); + comment on table credential_vault_library_ssh_private_key_mapping_override is + 'credential_vault_library_ssh_private_key_mapping_override is a table ' + 'where each row represents a mapping that overrides the default mapping ' + 'from a generic vault secret to a ssh private key credential type ' + 'for a vault credential library.'; + + create trigger insert_credential_vault_library_mapping_override_subtype before insert on credential_vault_library_ssh_private_key_mapping_override + for each row execute procedure insert_credential_vault_library_mapping_override_subtype(); + + create trigger delete_credential_vault_library_mapping_override_subtype after delete on credential_vault_library_ssh_private_key_mapping_override + for each row execute procedure delete_credential_vault_library_mapping_override_subtype(); + + -- Replaces view from 36/02_vault_private_library.up.sql + drop view credential_vault_library_public; + drop view credential_vault_library_private; + create view credential_vault_library_private as + with + password_override (library_id, username_attribute, password_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_username_password_mapping_override + ), + ssh_private_key_override (library_id, username_attribute, private_key_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(private_key_attribute, wt_to_sentinel('no override')) + from credential_vault_library_ssh_private_key_mapping_override + ) + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + library.http_method as http_method, + library.http_request_body as http_request_body, + library.credential_type as credential_type, + store.scope_id as scope_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.token_hmac as token_hmac, + store.ct_token as ct_token, -- encrypted + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + coalesce(upasso.username_attribute,sshpk.username_attribute) + as username_attribute, + upasso.password_attribute as password_attribute, + sshpk.private_key_attribute as private_key_attribute + from credential_vault_library library + join credential_vault_store_private store + on library.store_id = store.public_id + left join password_override upasso + on library.public_id = upasso.library_id + and store.token_status = 'current' + left join ssh_private_key_override sshpk + on library.public_id = sshpk.library_id + and store.token_status = 'current'; + comment on view credential_vault_library_private is + 'credential_vault_library_private is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' + 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + + -- Replaces view from 36/02_vault_private_library.up.sql + create view credential_vault_library_public as + select public_id, + store_id, + name, + description, + create_time, + update_time, + version, + vault_path, + http_method, + http_request_body, + credential_type, + username_attribute, + password_attribute, + private_key_attribute + from credential_vault_library_private; + comment on view credential_vault_library_public is + 'credential_vault_library_public is a view where each row contains a credential library and any of library''s credential mapping overrides. ' + 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; + +commit; diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index e5ebbc7c2..1238dad52 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -342,15 +342,20 @@ begin; ('vs_______wvs', 'kdkv___widget', 'current', 'hmac-value', 'token-value', now(), now() + interval '1 hour'); insert into credential_vault_library - (store_id, public_id, name, description, vault_path, http_method, credential_type) + (store_id, public_id, name, description, vault_path, http_method, credential_type) values - ('vs_______wvs', 'vl______wvl1', 'widget vault library', 'None', '/secrets', 'GET', 'unspecified'), - ('vs_______wvs', 'vl______wvl2', 'widget vault ssh', 'None', '/secrets/ssh/admin', 'GET', 'unspecified'), - ('vs_______wvs', 'vl______wvl3', 'widget vault kv one', 'None', '/secrets/kv/one', 'GET', 'username_password'), - ('vs_______wvs', 'vl______wvl4', 'widget vault kv two', 'None', '/secrets/kv/two', 'GET', 'username_password'), - ('vs_______wvs', 'vl______wvl5', 'widget vault kv three', 'None', '/secrets/kv/three', 'GET', 'username_password'), - ('vs_______wvs', 'vl______wvl6', 'widget vault kv four', 'None', '/secrets/kv/four', 'GET', 'username_password'), - ('vs_______wvs', 'vl______wvl7', 'widget vault kv five', 'None', '/secrets/kv/five', 'GET', 'username_password'); + ('vs_______wvs', 'vl______wvl1', 'widget vault library', 'None', '/secrets', 'GET', 'unspecified'), + ('vs_______wvs', 'vl______wvl2', 'widget vault ssh', 'None', '/secrets/ssh/admin', 'GET', 'unspecified'), + ('vs_______wvs', 'vl______wvl3', 'widget vault kv one', 'None', '/secrets/kv/one', 'GET', 'username_password'), + ('vs_______wvs', 'vl______wvl4', 'widget vault kv two', 'None', '/secrets/kv/two', 'GET', 'username_password'), + ('vs_______wvs', 'vl______wvl5', 'widget vault kv three', 'None', '/secrets/kv/three', 'GET', 'username_password'), + ('vs_______wvs', 'vl______wvl6', 'widget vault kv four', 'None', '/secrets/kv/four', 'GET', 'username_password'), + ('vs_______wvs', 'vl______wvl7', 'widget vault kv five', 'None', '/secrets/kv/five', 'GET', 'username_password'), + ('vs_______wvs', 'vl______wvl8', 'widget vault kv six', 'None', '/secrets/kv/six', 'GET', 'ssh_private_key'), + ('vs_______wvs', 'vl______wvl9', 'widget vault kv seven', 'None', '/secrets/kv/seven', 'GET', 'ssh_private_key'), + ('vs_______wvs', 'vl______wvl10', 'widget vault kv eight', 'None', '/secrets/kv/eight', 'GET', 'ssh_private_key'), + ('vs_______wvs', 'vl______wvl11', 'widget vault kv nine', 'None', '/secrets/kv/nine', 'GET', 'ssh_private_key'), + ('vs_______wvs', 'vl______wvl12', 'widget vault kv ten', 'None', '/secrets/kv/n', 'GET', 'ssh_private_key'); insert into credential_vault_library_username_password_mapping_override (library_id) @@ -372,6 +377,26 @@ begin; values ('vl______wvl7', 'my_username', 'my_password'); + insert into credential_vault_library_ssh_private_key_mapping_override + (library_id) + values + ('vl______wvl9'); + + insert into credential_vault_library_ssh_private_key_mapping_override + (library_id, username_attribute) + values + ('vl______wvl10', 'my_username'); + + insert into credential_vault_library_ssh_private_key_mapping_override + (library_id, private_key_attribute) + values + ('vl______wvl11', 'my_private_key'); + + insert into credential_vault_library_ssh_private_key_mapping_override + (library_id, username_attribute, private_key_attribute) + values + ('vl______wvl12', 'my_username', 'my_private_key'); + insert into target_credential_library (target_id, credential_library_id, credential_purpose) values diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql new file mode 100644 index 000000000..9382eae38 --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql @@ -0,0 +1,61 @@ +-- credential_vault_library_ssh_private_key_mapping_override tests: +-- the following triggers +-- insert_credential_vault_library_mapping_override_subtype +-- delete_credential_vault_library_mapping_override_subtype + +begin; + select plan(11); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate the setup data + select is(count(*), 4::bigint) + from credential_vault_library_ssh_private_key_mapping_override + where library_id in ('vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); + + select is(count(*), 4::bigint) + from credential_vault_library_mapping_override + where library_id in ('vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); + + prepare select_private_libraries as + select public_id::text, credential_type::text, username_attribute::text, private_key_attribute::text + from credential_vault_library_private + where public_id in ('vl______wvl2', 'vl______wvl8', 'vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12') + order by public_id; + + select results_eq( + 'select_private_libraries', + $$VALUES + ('vl______wvl10', 'ssh_private_key', 'my_username', null), + ('vl______wvl11', 'ssh_private_key', null, 'my_private_key'), + ('vl______wvl12', 'ssh_private_key', 'my_username', 'my_private_key'), + ('vl______wvl2', 'unspecified', null, null), + ('vl______wvl8', 'ssh_private_key', null, null), + ('vl______wvl9', 'ssh_private_key', null, null)$$ + ); + + -- validate the insert triggers + select is(count(*), 0::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8'; + select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8'; + + prepare insert_credential_vault_library_ssh_private_key_mapping_override as + insert into credential_vault_library_ssh_private_key_mapping_override + (library_id, username_attribute, private_key_attribute) + values + ('vl______wvl8', 'my_username', 'my_private_key'); + select lives_ok('insert_credential_vault_library_ssh_private_key_mapping_override'); + + select is(count(*), 1::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8'; + select is(count(*), 1::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8'; + + -- validate the delete triggers + prepare delete_credential_vault_library_ssh_private_key_mapping_override as + delete + from credential_vault_library_ssh_private_key_mapping_override + where library_id = 'vl______wvl8'; + select lives_ok('delete_credential_vault_library_ssh_private_key_mapping_override'); + + select is(count(*), 0::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8'; + select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8'; + + select * from finish(); +rollback; diff --git a/internal/gen/controller/servers/services/credential.pb.go b/internal/gen/controller/servers/services/credential.pb.go index b2777c18c..7a7ab0cfc 100644 --- a/internal/gen/controller/servers/services/credential.pb.go +++ b/internal/gen/controller/servers/services/credential.pb.go @@ -27,6 +27,7 @@ type Credential struct { // Types that are assignable to Credential: // *Credential_UsernamePassword + // *Credential_SshPrivateKey Credential isCredential_Credential `protobuf_oneof:"credential"` } @@ -76,6 +77,13 @@ func (x *Credential) GetUsernamePassword() *UsernamePassword { return nil } +func (x *Credential) GetSshPrivateKey() *SshPrivateKey { + if x, ok := x.GetCredential().(*Credential_SshPrivateKey); ok { + return x.SshPrivateKey + } + return nil +} + type isCredential_Credential interface { isCredential_Credential() } @@ -84,8 +92,14 @@ type Credential_UsernamePassword struct { UsernamePassword *UsernamePassword `protobuf:"bytes,2,opt,name=username_password,json=usernamePassword,proto3,oneof"` } +type Credential_SshPrivateKey struct { + SshPrivateKey *SshPrivateKey `protobuf:"bytes,3,opt,name=ssh_private_key,json=sshPrivateKey,proto3,oneof"` +} + func (*Credential_UsernamePassword) isCredential_Credential() {} +func (*Credential_SshPrivateKey) isCredential_Credential() {} + // UsernamePassword is a credential containing a username and a password. type UsernamePassword struct { state protoimpl.MessageState @@ -144,6 +158,64 @@ func (x *UsernamePassword) GetPassword() string { return "" } +// SshPrivateKey is a credential containing a username and a private key. +type SshPrivateKey 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"` +} + +func (x *SshPrivateKey) Reset() { + *x = SshPrivateKey{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_servers_services_v1_credential_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SshPrivateKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshPrivateKey) ProtoMessage() {} + +func (x *SshPrivateKey) ProtoReflect() protoreflect.Message { + mi := &file_controller_servers_services_v1_credential_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 SshPrivateKey.ProtoReflect.Descriptor instead. +func (*SshPrivateKey) Descriptor() ([]byte, []int) { + return file_controller_servers_services_v1_credential_proto_rawDescGZIP(), []int{2} +} + +func (x *SshPrivateKey) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *SshPrivateKey) GetPrivateKey() string { + if x != nil { + return x.PrivateKey + } + return "" +} + var File_controller_servers_services_v1_credential_proto protoreflect.FileDescriptor var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{ @@ -152,26 +224,37 @@ 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, 0x90, 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x31, 0x22, 0xe9, 0x01, 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, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x00, 0x52, 0x10, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 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, - 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, + 0x64, 0x12, 0x57, 0x0a, 0x0f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 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, 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, 0x4c, 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, 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 ( @@ -186,18 +269,20 @@ 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, 2) +var file_controller_servers_services_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 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 } 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 - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 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 } func init() { file_controller_servers_services_v1_credential_proto_init() } @@ -230,9 +315,22 @@ func file_controller_servers_services_v1_credential_proto_init() { return nil } } + file_controller_servers_services_v1_credential_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SshPrivateKey); 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), } type x struct{} out := protoimpl.TypeBuilder{ @@ -240,7 +338,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: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/proto/controller/api/resources/targets/v1/target.proto b/internal/proto/controller/api/resources/targets/v1/target.proto index 96df857cd..cde0572aa 100644 --- a/internal/proto/controller/api/resources/targets/v1/target.proto +++ b/internal/proto/controller/api/resources/targets/v1/target.proto @@ -288,3 +288,12 @@ message UsernamePasswordCredential { // Password of the credential string password = 2; // @gotags: `class:"secret"` } + +// The layout of the struct for "credential" field in SessionCredential for a ssh_private_key credential type. +message SshPrivateKeyCredential { + // Username of the credential + string username = 1; // @gotags: `class:"sensitive"` + + // Private key of the credential + string private_key = 2; // @gotags: `class:"secret"` +} diff --git a/internal/proto/controller/servers/services/v1/credential.proto b/internal/proto/controller/servers/services/v1/credential.proto index ea25185b6..f9c05beb9 100644 --- a/internal/proto/controller/servers/services/v1/credential.proto +++ b/internal/proto/controller/servers/services/v1/credential.proto @@ -10,6 +10,7 @@ message Credential { oneof credential { UsernamePassword username_password = 2; + SshPrivateKey ssh_private_key = 3; } } @@ -21,3 +22,12 @@ message UsernamePassword { // The password of the credential string password = 20; // @gotags: `class:"secret"` } + +// SshPrivateKey is a credential containing a username and a private key. +message SshPrivateKey { + // The username of the credential + string username = 10; // @gotags: `class:"public"` + + // The private key of the credential + string private_key = 20; // @gotags: `class:"secret"` +} 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 7c62b224d..dc8695b6c 100644 --- a/internal/proto/controller/storage/credential/vault/store/v1/vault.proto +++ b/internal/proto/controller/storage/credential/vault/store/v1/vault.proto @@ -344,3 +344,29 @@ message UsernamePasswordOverride { // @inject_tag: `gorm:"default:null"` string password_attribute = 3; } + +message SshPrivateKeyOverride { + // library_id of the owning vault credential library. + // @inject_tag: `gorm:"primary_key"` + string library_id = 1; + + // username_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a username. + // If set, it overrides any default attribute names the system uses to + // find a username attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + string username_attribute = 2; + + // private_key_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a private key. + // If set, it overrides any default attribute names the system uses to + // find a private_key attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + string private_key_attribute = 3; +} diff --git a/sdk/pbs/controller/api/resources/targets/target.pb.go b/sdk/pbs/controller/api/resources/targets/target.pb.go index 0752aec1e..1f7ed023c 100644 --- a/sdk/pbs/controller/api/resources/targets/target.pb.go +++ b/sdk/pbs/controller/api/resources/targets/target.pb.go @@ -1066,6 +1066,64 @@ func (x *UsernamePasswordCredential) GetPassword() string { return "" } +// The layout of the struct for "credential" field in SessionCredential for a ssh_private_key credential type. +type SshPrivateKeyCredential struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Username of the credential + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"` + // Private key of the credential + PrivateKey string `protobuf:"bytes,2,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty" class:"secret"` // @gotags: `class:"secret"` +} + +func (x *SshPrivateKeyCredential) Reset() { + *x = SshPrivateKeyCredential{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_targets_v1_target_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SshPrivateKeyCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshPrivateKeyCredential) ProtoMessage() {} + +func (x *SshPrivateKeyCredential) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_targets_v1_target_proto_msgTypes[11] + 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 SshPrivateKeyCredential.ProtoReflect.Descriptor instead. +func (*SshPrivateKeyCredential) Descriptor() ([]byte, []int) { + return file_controller_api_resources_targets_v1_target_proto_rawDescGZIP(), []int{11} +} + +func (x *SshPrivateKeyCredential) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *SshPrivateKeyCredential) GetPrivateKey() string { + if x != nil { + return x.PrivateKey + } + return "" +} + var File_controller_api_resources_targets_v1_target_proto protoreflect.FileDescriptor var file_controller_api_resources_targets_v1_target_proto_rawDesc = []byte{ @@ -1326,13 +1384,18 @@ var file_controller_api_resources_targets_v1_target_proto_rawDesc = []byte{ 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 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, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x50, - 0x5a, 0x4e, 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, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x3b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x56, + 0x0a, 0x17, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 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, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x50, 0x5a, 0x4e, 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, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, + 0x3b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1347,7 +1410,7 @@ func file_controller_api_resources_targets_v1_target_proto_rawDescGZIP() []byte return file_controller_api_resources_targets_v1_target_proto_rawDescData } -var file_controller_api_resources_targets_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_controller_api_resources_targets_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_controller_api_resources_targets_v1_target_proto_goTypes = []interface{}{ (*HostSource)(nil), // 0: controller.api.resources.targets.v1.HostSource (*HostSet)(nil), // 1: controller.api.resources.targets.v1.HostSet @@ -1360,38 +1423,39 @@ var file_controller_api_resources_targets_v1_target_proto_goTypes = []interface{ (*SessionAuthorizationData)(nil), // 8: controller.api.resources.targets.v1.SessionAuthorizationData (*SessionAuthorization)(nil), // 9: controller.api.resources.targets.v1.SessionAuthorization (*UsernamePasswordCredential)(nil), // 10: controller.api.resources.targets.v1.UsernamePasswordCredential - (*structpb.Struct)(nil), // 11: google.protobuf.Struct - (*scopes.ScopeInfo)(nil), // 12: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 13: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp - (*wrapperspb.UInt32Value)(nil), // 15: google.protobuf.UInt32Value - (*wrapperspb.Int32Value)(nil), // 16: google.protobuf.Int32Value + (*SshPrivateKeyCredential)(nil), // 11: controller.api.resources.targets.v1.SshPrivateKeyCredential + (*structpb.Struct)(nil), // 12: google.protobuf.Struct + (*scopes.ScopeInfo)(nil), // 13: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 14: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp + (*wrapperspb.UInt32Value)(nil), // 16: google.protobuf.UInt32Value + (*wrapperspb.Int32Value)(nil), // 17: google.protobuf.Int32Value } var file_controller_api_resources_targets_v1_target_proto_depIdxs = []int32{ - 11, // 0: controller.api.resources.targets.v1.SessionSecret.decoded:type_name -> google.protobuf.Struct + 12, // 0: controller.api.resources.targets.v1.SessionSecret.decoded:type_name -> google.protobuf.Struct 2, // 1: controller.api.resources.targets.v1.SessionCredential.credential_source:type_name -> controller.api.resources.targets.v1.CredentialSource 3, // 2: controller.api.resources.targets.v1.SessionCredential.secret:type_name -> controller.api.resources.targets.v1.SessionSecret - 11, // 3: controller.api.resources.targets.v1.SessionCredential.credential:type_name -> google.protobuf.Struct - 12, // 4: controller.api.resources.targets.v1.Target.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 13, // 5: controller.api.resources.targets.v1.Target.name:type_name -> google.protobuf.StringValue - 13, // 6: controller.api.resources.targets.v1.Target.description:type_name -> google.protobuf.StringValue - 14, // 7: controller.api.resources.targets.v1.Target.created_time:type_name -> google.protobuf.Timestamp - 14, // 8: controller.api.resources.targets.v1.Target.updated_time:type_name -> google.protobuf.Timestamp + 12, // 3: controller.api.resources.targets.v1.SessionCredential.credential:type_name -> google.protobuf.Struct + 13, // 4: controller.api.resources.targets.v1.Target.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 14, // 5: controller.api.resources.targets.v1.Target.name:type_name -> google.protobuf.StringValue + 14, // 6: controller.api.resources.targets.v1.Target.description:type_name -> google.protobuf.StringValue + 15, // 7: controller.api.resources.targets.v1.Target.created_time:type_name -> google.protobuf.Timestamp + 15, // 8: controller.api.resources.targets.v1.Target.updated_time:type_name -> google.protobuf.Timestamp 1, // 9: controller.api.resources.targets.v1.Target.host_sets:type_name -> controller.api.resources.targets.v1.HostSet 0, // 10: controller.api.resources.targets.v1.Target.host_sources:type_name -> controller.api.resources.targets.v1.HostSource - 15, // 11: controller.api.resources.targets.v1.Target.session_max_seconds:type_name -> google.protobuf.UInt32Value - 16, // 12: controller.api.resources.targets.v1.Target.session_connection_limit:type_name -> google.protobuf.Int32Value - 13, // 13: controller.api.resources.targets.v1.Target.worker_filter:type_name -> google.protobuf.StringValue + 16, // 11: controller.api.resources.targets.v1.Target.session_max_seconds:type_name -> google.protobuf.UInt32Value + 17, // 12: controller.api.resources.targets.v1.Target.session_connection_limit:type_name -> google.protobuf.Int32Value + 14, // 13: controller.api.resources.targets.v1.Target.worker_filter:type_name -> google.protobuf.StringValue 2, // 14: controller.api.resources.targets.v1.Target.application_credential_sources:type_name -> controller.api.resources.targets.v1.CredentialSource 2, // 15: controller.api.resources.targets.v1.Target.egress_credential_sources:type_name -> controller.api.resources.targets.v1.CredentialSource - 11, // 16: controller.api.resources.targets.v1.Target.attributes:type_name -> google.protobuf.Struct + 12, // 16: controller.api.resources.targets.v1.Target.attributes:type_name -> google.protobuf.Struct 6, // 17: controller.api.resources.targets.v1.Target.tcp_target_attributes:type_name -> controller.api.resources.targets.v1.TcpTargetAttributes - 15, // 18: controller.api.resources.targets.v1.TcpTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value - 12, // 19: controller.api.resources.targets.v1.SessionAuthorizationData.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 14, // 20: controller.api.resources.targets.v1.SessionAuthorizationData.created_time:type_name -> google.protobuf.Timestamp + 16, // 18: controller.api.resources.targets.v1.TcpTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value + 13, // 19: controller.api.resources.targets.v1.SessionAuthorizationData.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 15, // 20: controller.api.resources.targets.v1.SessionAuthorizationData.created_time:type_name -> google.protobuf.Timestamp 7, // 21: controller.api.resources.targets.v1.SessionAuthorizationData.worker_info:type_name -> controller.api.resources.targets.v1.WorkerInfo - 12, // 22: controller.api.resources.targets.v1.SessionAuthorization.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 14, // 23: controller.api.resources.targets.v1.SessionAuthorization.created_time:type_name -> google.protobuf.Timestamp + 13, // 22: controller.api.resources.targets.v1.SessionAuthorization.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 15, // 23: controller.api.resources.targets.v1.SessionAuthorization.created_time:type_name -> google.protobuf.Timestamp 4, // 24: controller.api.resources.targets.v1.SessionAuthorization.credentials:type_name -> controller.api.resources.targets.v1.SessionCredential 25, // [25:25] is the sub-list for method output_type 25, // [25:25] is the sub-list for method input_type @@ -1538,6 +1602,18 @@ func file_controller_api_resources_targets_v1_target_proto_init() { return nil } } + file_controller_api_resources_targets_v1_target_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SshPrivateKeyCredential); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_api_resources_targets_v1_target_proto_msgTypes[5].OneofWrappers = []interface{}{ (*Target_Attributes)(nil), @@ -1549,7 +1625,7 @@ func file_controller_api_resources_targets_v1_target_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_targets_v1_target_proto_rawDesc, NumEnums: 0, - NumMessages: 11, + NumMessages: 12, NumExtensions: 0, NumServices: 0, },