From 205cb64d8b0ca415624fd0578e9b849cc3e2acd7 Mon Sep 17 00:00:00 2001 From: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:21:15 -0500 Subject: [PATCH] feat(credential/static): Implement CRUDL support for password credentials (#6163) --- internal/credential/public_ids.go | 6 +- internal/credential/static/credential.go | 18 + .../credential/static/password_credential.go | 141 +++ .../static/password_credential_test.go | 164 ++++ internal/credential/static/query.go | 95 ++ .../static/repository_credential.go | 221 +++++ .../static/repository_credential_test.go | 854 +++++++++++++++--- .../static/repository_credentials.go | 22 +- .../static/repository_credentials_test.go | 27 +- internal/credential/static/rewrapping.go | 46 + internal/credential/static/rewrapping_test.go | 64 ++ internal/credential/static/store/static.pb.go | 244 ++++- internal/credential/static/testing.go | 71 ++ .../credential/static/store/v1/static.proto | 63 ++ 14 files changed, 1881 insertions(+), 155 deletions(-) create mode 100644 internal/credential/static/password_credential.go create mode 100644 internal/credential/static/password_credential_test.go diff --git a/internal/credential/public_ids.go b/internal/credential/public_ids.go index 9cee87659e..b5a04e960b 100644 --- a/internal/credential/public_ids.go +++ b/internal/credential/public_ids.go @@ -51,11 +51,11 @@ func NewUsernamePasswordDomainCredentialId(ctx context.Context) (string, error) return id, nil } -// PasswordCredentialId generates a new public ID for a password credential. -func PasswordCredentialId(ctx context.Context) (string, error) { +// NewPasswordCredentialId generates a new public ID for a password credential. +func NewPasswordCredentialId(ctx context.Context) (string, error) { id, err := db.NewPublicId(ctx, globals.PasswordCredentialPrefix) if err != nil { - return "", errors.Wrap(ctx, err, "credential.PasswordCredentialId") + return "", errors.Wrap(ctx, err, "credential.NewPasswordCredentialId") } return id, nil } diff --git a/internal/credential/static/credential.go b/internal/credential/static/credential.go index fd7665239a..2c992bb7d8 100644 --- a/internal/credential/static/credential.go +++ b/internal/credential/static/credential.go @@ -92,6 +92,24 @@ func (c *listCredentialResult) toCredential(ctx context.Context) (credential.Sta cred.PasswordHmac = []byte(c.Hmac1) } return cred, nil + case "p": + cred := &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + PublicId: c.PublicId, + StoreId: c.StoreId, + Name: c.Name, + Description: c.Description, + CreateTime: c.CreateTime, + UpdateTime: c.UpdateTime, + Version: uint32(c.Version), + KeyId: c.KeyId, + }, + } + // Assign byte slices only if the string isn't empty + if c.Hmac1 != "" { + cred.PasswordHmac = []byte(c.Hmac1) + } + return cred, nil case "ssh": cred := &SshPrivateKeyCredential{ SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ diff --git a/internal/credential/static/password_credential.go b/internal/credential/static/password_credential.go new file mode 100644 index 0000000000..e2620befd7 --- /dev/null +++ b/internal/credential/static/password_credential.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package static + +import ( + "context" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/static/store" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/libs/crypto" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/types/resource" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" + "google.golang.org/protobuf/proto" +) + +var _ credential.Static = (*PasswordCredential)(nil) + +// PasswordCredential contains the credential with a password. +// It is owned by a credential store. +type PasswordCredential struct { + *store.PasswordCredential + tableName string `gorm:"-"` +} + +// NewPasswordCredential creates a new in memory static Credential containing a +// password that is assigned to storeId. Name and description are the only +// valid options. All other options are ignored. +func NewPasswordCredential( + storeId string, + password credential.Password, + opt ...Option, +) (*PasswordCredential, error) { + opts := getOpts(opt...) + l := &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + StoreId: storeId, + Name: opts.withName, + Description: opts.withDescription, + Password: []byte(password), + }, + } + return l, nil +} + +func allocPasswordCredential() *PasswordCredential { + return &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{}, + } +} + +func (c *PasswordCredential) clone() *PasswordCredential { + cp := proto.Clone(c.PasswordCredential) + return &PasswordCredential{ + PasswordCredential: cp.(*store.PasswordCredential), + } +} + +// TableName returns the table name. +func (c *PasswordCredential) TableName() string { + if c.tableName != "" { + return c.tableName + } + return "credential_static_password_credential" +} + +// SetTableName sets the table name. +func (c *PasswordCredential) SetTableName(n string) { + c.tableName = n +} + +// GetResourceType returns the resource type of the Credential +func (c *PasswordCredential) GetResourceType() resource.Type { + return resource.Credential +} + +func (c *PasswordCredential) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{c.PublicId}, + "resource-type": []string{"credential-static-password"}, + "op-type": []string{op.String()}, + } + if c.StoreId != "" { + metadata["store-id"] = []string{c.StoreId} + } + return metadata +} + +func (c *PasswordCredential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(PasswordCredential).encrypt" + if len(c.Password) == 0 { + return errors.New(ctx, errors.InvalidParameter, op, "no password defined") + } + if err := structwrapping.WrapStruct(ctx, cipher, c.PasswordCredential, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + keyId, err := cipher.KeyId(ctx) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("error reading cipher key id")) + } + c.KeyId = keyId + if err := c.hmacPassword(ctx, cipher); err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +func (c *PasswordCredential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(PasswordCredential).decrypt" + if err := structwrapping.UnwrapStruct(ctx, cipher, c.PasswordCredential, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} + +func (c *PasswordCredential) hmacPassword(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(PasswordCredential).hmacPassword" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + hm, err := crypto.HmacSha256(ctx, c.Password, cipher, []byte(c.StoreId), nil, crypto.WithEd25519()) + if err != nil { + return errors.Wrap(ctx, err, op) + } + c.PasswordHmac = []byte(hm) + return nil +} + +type deletedPasswordCredential struct { + PublicId string `gorm:"primary_key"` + DeleteTime *timestamp.Timestamp +} + +// TableName returns the tablename to override the default gorm table name +func (s *deletedPasswordCredential) TableName() string { + return "credential_static_password_credential_deleted" +} diff --git a/internal/credential/static/password_credential_test.go b/internal/credential/static/password_credential_test.go new file mode 100644 index 0000000000..9600783a3f --- /dev/null +++ b/internal/credential/static/password_credential_test.go @@ -0,0 +1,164 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package static + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/static/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestPasswordCredential_New(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kkms := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + + type args struct { + password credential.Password + storeId string + options []Option + } + + tests := []struct { + name string + args args + want *PasswordCredential + wantCreateErr bool + wantEncryptErr bool + }{ + { + name: "missing-password", + args: args{ + storeId: cs.PublicId, + }, + want: allocPasswordCredential(), + wantEncryptErr: true, + }, + { + name: "missing-store-id", + args: args{ + password: "test-pass", + }, + want: allocPasswordCredential(), + wantCreateErr: true, + }, + { + name: "valid-no-options", + args: args{ + password: "test-pass", + storeId: cs.PublicId, + }, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("test-pass"), + StoreId: cs.PublicId, + }, + }, + }, + { + name: "valid-with-name", + args: args{ + password: "test-pass", + storeId: cs.PublicId, + options: []Option{WithName("my-credential")}, + }, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("test-pass"), + StoreId: cs.PublicId, + Name: "my-credential", + }, + }, + }, + { + name: "valid-with-description", + args: args{ + password: "test-pass", + storeId: cs.PublicId, + options: []Option{WithDescription("my-credential-description")}, + }, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("test-pass"), + StoreId: cs.PublicId, + Description: "my-credential-description", + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + + got, err := NewPasswordCredential(tt.args.storeId, tt.args.password, tt.args.options...) + + require.NoError(err) + require.NotNil(got) + assert.Emptyf(got.PublicId, "PublicId set") + id, err := credential.NewPasswordCredentialId(ctx) + require.NoError(err) + + tt.want.PublicId = id + got.PublicId = id + + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + err = got.encrypt(ctx, databaseWrapper) + if tt.wantEncryptErr { + require.Error(err) + return + } + assert.NoError(err) + + err = rw.Create(context.Background(), got) + if tt.wantCreateErr { + require.Error(err) + return + } + assert.NoError(err) + + got2 := allocPasswordCredential() + got2.PublicId = id + assert.Equal(id, got2.GetPublicId()) + require.NoError(rw.LookupById(ctx, got2)) + + err = got2.decrypt(ctx, databaseWrapper) + require.NoError(err) + + // Timestamps and version are automatically set + tt.want.CreateTime = got2.CreateTime + tt.want.UpdateTime = got2.UpdateTime + tt.want.Version = got2.Version + + // KeyId is allocated via kms no need to validate in this test + tt.want.KeyId = got2.KeyId + got2.CtPassword = nil + + // encrypt also calculates the hmac, validate it is correct + hm, err := crypto.HmacSha256(ctx, got.Password, databaseWrapper, []byte(got.StoreId), nil, crypto.WithEd25519()) + require.NoError(err) + tt.want.PasswordHmac = []byte(hm) + + assert.Empty(cmp.Diff(tt.want, got2.clone(), protocmp.Transform())) + }) + } +} diff --git a/internal/credential/static/query.go b/internal/credential/static/query.go index 9fbbab6fa4..4225b0fa11 100644 --- a/internal/credential/static/query.go +++ b/internal/credential/static/query.go @@ -26,6 +26,16 @@ select distinct upd.public_id, and upd.key_id = ?; ` + credStaticPasswordRewrapQuery = ` +select distinct pass.public_id, + pass.password_encrypted, + pass.key_id + from credential_static_password_credential pass + inner join credential_static_store store + on store.public_id = pass.store_id + where store.project_id = ? + and pass.key_id = ?; +` credStaticSshPrivKeyRewrapQuery = ` select distinct ssh.public_id, ssh.private_key_encrypted, @@ -56,6 +66,7 @@ select sum(reltuples::bigint) as estimate 'credential_static_json_credential'::regclass, 'credential_static_username_password_credential'::regclass, 'credential_static_username_password_domain_credential'::regclass, + 'credential_static_password_credential'::regclass, 'credential_static_ssh_private_key_credential'::regclass ) ` @@ -83,6 +94,11 @@ upd_creds as ( from credential_static_username_password_domain_credential where public_id in (select public_id from credentials) ), +p_creds as ( + select * + from credential_static_password_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -137,6 +153,22 @@ final as ( 'upd' as type from upd_creds union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + null as username, -- Add this to make the union uniform + null as domain, -- Add this to make the union uniform + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'p' as type + from p_creds + union select public_id, store_id, project_id, @@ -182,6 +214,11 @@ upd_creds as ( from credential_static_username_password_domain_credential where public_id in (select public_id from credentials) ), +p_creds as ( + select * + from credential_static_password_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -236,6 +273,22 @@ final as ( 'upd' as type from upd_creds union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + null as username, -- Add this to make the union uniform + null as domain, -- Add this to make the union uniform + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'p' as type + from p_creds + union select public_id, store_id, project_id, @@ -281,6 +334,11 @@ upd_creds as ( from credential_static_username_password_domain_credential where public_id in (select public_id from credentials) ), +p_creds as ( + select * + from credential_static_password_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -335,6 +393,22 @@ final as ( 'upd' as type from upd_creds union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + null as username, -- Add this to make the union uniform + null as domain, -- Add this to make the union uniform + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'p' as type + from p_creds + union select public_id, store_id, project_id, @@ -381,6 +455,11 @@ upd_creds as ( from credential_static_username_password_domain_credential where public_id in (select public_id from credentials) ), +p_creds as ( + select * + from credential_static_password_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -435,6 +514,22 @@ final as ( 'upd' as type from upd_creds union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + null as username, -- Add this to make the union uniform + null as domain, -- Add this to make the union uniform + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'p' as type + from p_creds + union select public_id, store_id, project_id, diff --git a/internal/credential/static/repository_credential.go b/internal/credential/static/repository_credential.go index 4b851ec5a6..f5ca8d304f 100644 --- a/internal/credential/static/repository_credential.go +++ b/internal/credential/static/repository_credential.go @@ -196,6 +196,89 @@ func (r *Repository) CreateUsernamePasswordDomainCredential( return newCred, nil } +// CreatePasswordCredential inserts c into the repository and returns a new +// PasswordCredential containing the credential's PublicId. c is not +// changed. c must not contain a PublicId. The PublicId is generated and +// assigned by this method. c must contain a valid StoreId. +// +// The password is encrypted and a HmacSha256 of the password is calculated. Only the +// PasswordHmac is returned, the plain-text and encrypted password is not returned. +// +// Both c.Name and c.Description are optional. If c.Name is set, it must +// be unique within c.ProjectId. Both c.CreateTime and c.UpdateTime are +// ignored. +func (r *Repository) CreatePasswordCredential( + ctx context.Context, + projectId string, + c *PasswordCredential, + _ ...Option, +) (*PasswordCredential, error) { + const op = "static.(Repository).CreatePasswordCredential" + if c == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing credential") + } + if c.PasswordCredential == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing embedded credential") + } + if projectId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + if c.Password == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password") + } + if c.StoreId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing store id") + } + if c.PublicId != "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + + c = c.clone() + id, err := credential.NewPasswordCredentialId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + c.PublicId = id + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + // encrypt + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.encrypt(ctx, databaseWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var newCred *PasswordCredential + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newCred = c.clone() + if err := w.Create(ctx, newCred, + db.WithOplog(oplogWrapper, newCred.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil + }, + ) + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in store: %s: name %s already exists", c.StoreId, c.Name))) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in store: %s", c.StoreId))) + } + + // Clear password fields, only PasswordHmac should be returned + newCred.CtPassword = nil + newCred.Password = nil + + return newCred, nil +} + // CreateSshPrivateKeyCredential inserts c into the repository and returns a new // SshPrivateKeyCredential containing the credential's PublicId. c is not // changed. c must not contain a PublicId. The PublicId is generated and @@ -411,6 +494,20 @@ func (r *Repository) LookupCredential(ctx context.Context, publicId string, _ .. updCred.Password = nil cred = updCred + case credential.PasswordSubtype: + pCred := allocPasswordCredential() + pCred.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, pCred); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", publicId))) + } + // Clear password fields, only passwordHmac should be returned + pCred.CtPassword = nil + pCred.Password = nil + cred = pCred + case credential.SshPrivateKeySubtype: spkCred := allocSshPrivateKeyCredential() spkCred.PublicId = publicId @@ -676,6 +773,118 @@ func (r *Repository) UpdateUsernamePasswordDomainCredential(ctx context.Context, return returnedCredential, rowsUpdated, nil } +// UpdatePasswordCredential updates the repository entry for c.PublicId with +// the values in c for the fields listed in fieldMaskPaths. It returns a +// new PasswordCredential containing the updated values and a count of the +// number of records updated. c is not changed. +// +// c must contain a valid PublicId. Only Name, Description and Password can be +// changed. If c.Name is set to a non-empty string, it must be unique within c.ProjectId. +// +// An attribute of c will be set to NULL in the database if the attribute +// in c is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdatePasswordCredential(ctx context.Context, + projectId string, + c *PasswordCredential, + version uint32, + fieldMaskPaths []string, + _ ...Option, +) (*PasswordCredential, int, error) { + const op = "static.(Repository).UpdatePasswordCredential" + if c == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing credential") + } + if c.PasswordCredential == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded credential") + } + if c.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + if version == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + } + if projectId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + if c.StoreId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing store id") + } + c = c.clone() + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(nameField, f): + case strings.EqualFold(descriptionField, f): + case strings.EqualFold(passwordField, f): + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) + } + } + dbMask, nullFields := dbw.BuildUpdatePaths( + map[string]any{ + nameField: c.Name, + descriptionField: c.Description, + passwordField: c.Password, + }, + fieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask") + } + + for _, f := range fieldMaskPaths { + if strings.EqualFold(passwordField, f) { + // Password has been updated, re-encrypt and recalculate hmac + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.encrypt(ctx, databaseWrapper); err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + + // Set PasswordHmac and CtPassword masks for update. + dbMask = append(dbMask, "PasswordHmac", "CtPassword", "KeyId") + } + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, + errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsUpdated int + var returnedCredential *PasswordCredential + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedCredential = c.clone() + var err error + rowsUpdated, err = w.Update(ctx, returnedCredential, + dbMask, nullFields, + db.WithOplog(oplogWrapper, returnedCredential.oplog(oplog.OpType_OP_TYPE_UPDATE)), + db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated") + } + return nil + }, + ) + if err != nil { + return nil, db.NoRowsAffected, err + } + + // Clear password fields, only PasswordHmac should be returned + returnedCredential.CtPassword = nil + returnedCredential.Password = nil + + return returnedCredential, rowsUpdated, nil +} + // UpdateSshPrivateKeyCredential updates the repository entry for c.PublicId // with the values in c for the fields listed in fieldMaskPaths. It returns a // new SshPrivateKeyCredential containing the updated values and a count of the @@ -1089,6 +1298,11 @@ func (r *Repository) DeleteCredential(ctx context.Context, projectId, id string, c.PublicId = id input = c md = c.oplog(oplog.OpType_OP_TYPE_DELETE) + case credential.PasswordSubtype: + c := allocPasswordCredential() + c.PublicId = id + input = c + md = c.oplog(oplog.OpType_OP_TYPE_DELETE) case credential.SshPrivateKeySubtype: c := allocSshPrivateKeyCredential() c.PublicId = id @@ -1177,6 +1391,13 @@ func (r *Repository) ListDeletedCredentialIds(ctx context.Context, since time.Ti for _, cl := range deletedUsernamePasswordDomainCredentials { credentialStoreIds = append(credentialStoreIds, cl.PublicId) } + var deletedPasswordCredentials []*deletedPasswordCredential + if err := r.SearchWhere(ctx, &deletedPasswordCredentials, "delete_time >= ?", []any{since}); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted password credentials")) + } + for _, cl := range deletedPasswordCredentials { + credentialStoreIds = append(credentialStoreIds, cl.PublicId) + } var deletedSSHPrivateKeyCredentials []*deletedSSHPrivateKeyCredential if err := r.SearchWhere(ctx, &deletedSSHPrivateKeyCredentials, "delete_time >= ?", []any{since}); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted ssh private key credentials")) diff --git a/internal/credential/static/repository_credential_test.go b/internal/credential/static/repository_credential_test.go index 30293e26b0..ee30655570 100644 --- a/internal/credential/static/repository_credential_test.go +++ b/internal/credential/static/repository_credential_test.go @@ -391,6 +391,162 @@ func TestRepository_CreateUsernamePasswordDomainCredential(t *testing.T) { }) } +func TestRepository_CreatePasswordCredential(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + + tests := []struct { + name string + projectId string + cred *PasswordCredential + wantErr bool + wantErrCode errors.Code + }{ + { + name: "missing-store", + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-embedded-cred", + cred: &PasswordCredential{}, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-project-id", + cred: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("secret"), + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-password", + projectId: prj.PublicId, + cred: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-store-id", + projectId: prj.PublicId, + cred: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("secret"), + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "valid", + projectId: prj.PublicId, + cred: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("secret"), + StoreId: cs.PublicId, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + require.NoError(err) + require.NotNil(repo) + + got, err := repo.CreatePasswordCredential(ctx, tt.projectId, tt.cred) + if tt.wantErr { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assertPublicId(t, globals.PasswordCredentialPrefix, got.PublicId) + assert.Nil(got.Password) + assert.Nil(got.CtPassword) + + // Validate password + lookupCred := allocPasswordCredential() + lookupCred.PublicId = got.PublicId + require.NoError(rw.LookupById(ctx, lookupCred)) + + databaseWrapper, err := kkms.GetWrapper(context.Background(), tt.projectId, kms.KeyPurposeDatabase) + require.NoError(err) + require.NoError(lookupCred.decrypt(ctx, databaseWrapper)) + assert.Equal(tt.cred.Password, lookupCred.Password) + + assert.Empty(got.Password) + assert.Empty(got.CtPassword) + assert.NotEmpty(got.PasswordHmac) + + // Validate hmac + hm, err := crypto.HmacSha256(ctx, tt.cred.Password, databaseWrapper, []byte(tt.cred.StoreId), nil, crypto.WithEd25519()) + require.NoError(err) + assert.Equal([]byte(hm), got.PasswordHmac) + + // Validate oplog + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } + + t.Run("duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + prj2 := iam.TestProject(t, iam.TestRepo(t, conn, wrapper), org.GetPublicId()) + require.NoError(err) + + prjCs := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + prj2Cs := TestCredentialStore(t, conn, wrapper, prj2.GetPublicId()) + + in, err := NewPasswordCredential(prjCs.GetPublicId(), "pass", WithName("my-name"), WithDescription("original")) + assert.NoError(err) + + got, err := repo.CreatePasswordCredential(ctx, prj.PublicId, in) + require.NoError(err) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + + in2, err := NewPasswordCredential(prjCs.GetPublicId(), "pass", WithName("my-name"), WithDescription("different")) + require.NoError(err) + got2, err := repo.CreatePasswordCredential(ctx, prj.GetPublicId(), in2) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err code: %v got err: %v", errors.NotUnique, err) + assert.Nil(got2) + + // Creating credential in different project should not conflict + in3, err := NewPasswordCredential(prj2Cs.GetPublicId(), "pass", WithName("my-name"), WithDescription("different")) + require.NoError(err) + got3, err := repo.CreatePasswordCredential(ctx, prj2.GetPublicId(), in3) + require.NoError(err) + assert.Equal(in3.Name, got3.Name) + assert.Equal(in3.Description, got3.Description) + + assert.NotEqual(got.PublicId, got3.PublicId) + }) +} + func TestRepository_CreateSshPrivateKeyCredential(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") @@ -766,6 +922,7 @@ func TestRepository_LookupCredential(t *testing.T) { store := TestCredentialStore(t, conn, wrapper, prj.PublicId) upCred := TestUsernamePasswordCredential(t, conn, wrapper, "username", "password", store.PublicId, prj.PublicId) updCred := TestUsernamePasswordDomainCredential(t, conn, wrapper, "username", "password", "domain.com", store.PublicId, prj.PublicId) + pCred := TestPasswordCredential(t, conn, wrapper, "password", store.PublicId, prj.PublicId) spkCred := TestSshPrivateKeyCredential(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.PublicId, prj.PublicId) spkCredWithPass := TestSshPrivateKeyCredential(t, conn, wrapper, "username", string(testdata.PEMEncryptedKeys[0].PEMBytes), store.PublicId, prj.PublicId, WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey))) @@ -790,6 +947,11 @@ func TestRepository_LookupCredential(t *testing.T) { id: updCred.GetPublicId(), want: updCred, }, + { + name: "p-valid", + id: pCred.GetPublicId(), + want: pCred, + }, { name: "spk-valid", id: spkCred.GetPublicId(), @@ -849,6 +1011,10 @@ func TestRepository_LookupCredential(t *testing.T) { assert.Empty(v.Password) assert.Empty(v.CtPassword) assert.NotEmpty(v.PasswordHmac) + case *PasswordCredential: + assert.Empty(v.Password) + assert.Empty(v.CtPassword) + assert.NotEmpty(v.PasswordHmac) case *SshPrivateKeyCredential: assert.Empty(v.PrivateKey) assert.Empty(v.PrivateKeyEncrypted) @@ -884,6 +1050,7 @@ func TestRepository_ListCredentials(t *testing.T) { store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), total/4) TestUsernamePasswordDomainCredentials(t, conn, wrapper, "user", "pass", "domain.com", store.GetPublicId(), prj.GetPublicId(), total/4) + TestPasswordCredentials(t, conn, wrapper, "pass", store.GetPublicId(), prj.GetPublicId(), total/4) TestSshPrivateKeyCredentials(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), total/4) obj, _ := TestJsonObject(t) @@ -948,6 +1115,10 @@ func TestRepository_ListCredentials(t *testing.T) { assert.Empty(v.Password) assert.Empty(v.CtPassword) assert.NotEmpty(v.PasswordHmac) + case *PasswordCredential: + assert.Empty(v.Password) + assert.Empty(v.CtPassword) + assert.NotEmpty(v.PasswordHmac) case *SshPrivateKeyCredential: assert.Empty(v.PrivateKey) assert.Empty(v.PrivateKeyEncrypted) @@ -979,6 +1150,7 @@ func TestRepository_ListCredentials_Pagination(t *testing.T) { _ = TestSshPrivateKeyCredentials(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), 2) _ = TestUsernamePasswordCredentials(t, conn, wrapper, "username", "testpassword", store.GetPublicId(), prj.GetPublicId(), 2) _ = TestUsernamePasswordDomainCredentials(t, conn, wrapper, "username", "testpassword", "domain.com", store.GetPublicId(), prj.GetPublicId(), 1) + _ = TestPasswordCredentials(t, conn, wrapper, "testpassword", store.GetPublicId(), prj.GetPublicId(), 1) repo, err := NewRepository(ctx, rw, rw, kms) require.NoError(err) @@ -1015,15 +1187,16 @@ func TestRepository_ListCredentials_Pagination(t *testing.T) { page4, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page3[1])) require.NoError(err) - require.Len(page4, 1) + require.Len(page4, 2) pages = append(pages, page3...) for _, item := range pages { assert.NotEqual(item.GetPublicId(), page4[0].GetPublicId()) + assert.NotEqual(item.GetPublicId(), page4[1].GetPublicId()) } assert.True(time.Now().Before(ttime.Add(10 * time.Second))) assert.True(time.Now().After(ttime.Add(-10 * time.Second))) - page5, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page4[0])) + page5, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page4[1])) require.NoError(err) require.Empty(page5) assert.True(time.Now().Before(ttime.Add(10 * time.Second))) @@ -2292,133 +2465,68 @@ func TestRepository_UpdateUsernamePasswordDomainCredential(t *testing.T) { } } -func TestRepository_UpdatePasswordCredentialKeyUpdate(t *testing.T) { - t.Parallel() - conn, _ := db.TestSetup(t, "postgres") - rw := db.New(conn) - wrapper := db.TestWrapper(t) - assert, require := assert.New(t), require.New(t) - ctx := context.Background() - kkms := kms.TestKms(t, conn, wrapper) - repo, err := NewRepository(ctx, rw, rw, kkms) - require.NoError(err) - - _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - credStore := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) - orig, err := repo.CreateUsernamePasswordCredential(ctx, prj.GetPublicId(), &UsernamePasswordCredential{ - UsernamePasswordCredential: &store.UsernamePasswordCredential{ - Username: "user", - Password: []byte("pass"), - StoreId: credStore.PublicId, - }, - }) - require.NoError(err) - - err = kkms.RotateKeys(ctx, prj.GetPublicId()) - require.NoError(err) - - orig.Password = []byte("pass1") // Company policy to change password every 3 months - - got, _, err := repo.UpdateUsernamePasswordCredential(ctx, prj.GetPublicId(), orig, orig.GetVersion(), []string{"Password"}) - require.NoError(err) - - // Validate that the KeyId has changed - assert.NotEqual(orig.KeyId, got.KeyId) - - // Validate hmac - databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.KeyId)) - require.NoError(err) - hm, err := crypto.HmacSha256(ctx, orig.Password, databaseWrapper, []byte(credStore.GetPublicId()), nil, crypto.WithEd25519()) - require.NoError(err) - assert.Equal([]byte(hm), got.PasswordHmac) -} - -func TestRepository_UpdateSshPrivateKeyCredential(t *testing.T) { - const testSecondarySshPrivateKeyPem = ` ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQAAAJDmpbfr5qW3 -6wAAAAtzc2gtZWQyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQ -AAAEBvvkQkH06ad2GpX1VVARzu9NkHA6gzamAaQ/hkn5FuZvF/CEQBkquextDE6NUDc8WI -Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= ------END OPENSSH PRIVATE KEY----- -` - +func TestRepository_UpdatePasswordCredential(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - changeName := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + changeName := func(n string) func(credential *PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.Name = n return c } } - changeDescription := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + changeDescription := func(d string) func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.Description = d return c } } - makeNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential { + makeNil := func() func(*PasswordCredential) *PasswordCredential { + return func(_ *PasswordCredential) *PasswordCredential { return nil } } - makeEmbeddedNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential { - return &SshPrivateKeyCredential{} + makeEmbeddedNil := func() func(*PasswordCredential) *PasswordCredential { + return func(_ *PasswordCredential) *PasswordCredential { + return &PasswordCredential{} } } - setPublicId := func(n string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + setPublicId := func(n string) func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.PublicId = n return c } } - deleteStoreId := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + deleteStoreId := func() func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.StoreId = "" return c } } - deleteVersion := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + deleteVersion := func() func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.Version = 0 return c } } - changeUser := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { - c.Username = n - return c - } - } - - changePrivateKey := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { - c.PrivateKey = []byte(d) - return c - } - } - - changePrivateKeyPassphrase := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { - c.PrivateKeyPassphrase = []byte(d) + changePassword := func(d string) func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { + c.Password = []byte(d) return c } } - combine := func(fns ...func(cs *SshPrivateKeyCredential) *SshPrivateKeyCredential) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + combine := func(fns ...func(cs *PasswordCredential) *PasswordCredential) func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { for _, fn := range fns { c = fn(c) } @@ -2428,19 +2536,18 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= tests := []struct { name string - orig *SshPrivateKeyCredential - chgFn func(*SshPrivateKeyCredential) *SshPrivateKeyCredential + orig *PasswordCredential + chgFn func(*PasswordCredential) *PasswordCredential masks []string - want *SshPrivateKeyCredential + want *PasswordCredential wantCount int wantErr errors.Code }{ { name: "nil-credential", - orig: &SshPrivateKeyCredential{ - SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ - Username: "user", - PrivateKey: []byte(TestSshPrivateKeyPem), + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), }, }, chgFn: makeNil(), @@ -2449,10 +2556,9 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= }, { name: "nil-embedded-credential", - orig: &SshPrivateKeyCredential{ - SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ - Username: "user", - PrivateKey: []byte(TestSshPrivateKeyPem), + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), }, }, chgFn: makeEmbeddedNil(), @@ -2461,10 +2567,9 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= }, { name: "no-public-id", - orig: &SshPrivateKeyCredential{ - SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ - Username: "user", - PrivateKey: []byte(TestSshPrivateKeyPem), + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), }, }, chgFn: setPublicId(""), @@ -2473,10 +2578,9 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= }, { name: "no-store-id", - orig: &SshPrivateKeyCredential{ - SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ - Username: "user", - PrivateKey: []byte(TestSshPrivateKeyPem), + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), }, }, chgFn: deleteStoreId(), @@ -2485,7 +2589,502 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= }, { name: "no-version", - orig: &SshPrivateKeyCredential{ + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), + }, + }, + chgFn: deleteVersion(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "updating-non-existent-credential", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: combine(setPublicId("abcd_OOOOOOOOOO"), changeName("test-update-name-repo")), + masks: []string{"Name"}, + wantErr: errors.RecordNotFound, + }, + { + name: "empty-field-mask", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeName("test-update-name-repo"), + wantErr: errors.EmptyFieldMask, + }, + { + name: "read-only-fields-in-field-mask", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "ProjectId"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "unknown-field-in-field-mask", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Bilbo"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "change-name", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Name"}, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-update-name-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "change-description", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{"Description"}, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Description: "test-update-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")), + masks: []string{"Name", "Description"}, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-update-name-repo", + Description: "test-update-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "change-password", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), + }, + }, + chgFn: changePassword("test-update-pass"), + masks: []string{"Password"}, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("test-update-pass"), + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-password", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeName("new-name"), changePassword("")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "new-name", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-update-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-update-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + tt.orig.StoreId = store.PublicId + + orig, err := repo.CreatePasswordCredential(ctx, prj.GetPublicId(), tt.orig) + assert.NoError(err) + require.NotNil(orig) + + if tt.chgFn != nil { + orig = tt.chgFn(orig) + } + var version uint32 + if orig != nil { + version = orig.GetVersion() + } + got, gotCount, err := repo.UpdatePasswordCredential(ctx, prj.GetPublicId(), orig, version, tt.masks) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + assert.NoError(err) + assert.Empty(tt.orig.PublicId) + require.NotNil(got) + assertPublicId(t, globals.PasswordCredentialPrefix, got.PublicId) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.NotSame(tt.orig, got) + assert.Equal(tt.orig.StoreId, got.StoreId) + underlyingDB, err := conn.SqlDB(ctx) + require.NoError(err) + dbassert := dbassert.New(t, underlyingDB) + if tt.want.Name == "" { + got := got.clone() + dbassert.IsNull(got, "name") + } else { + assert.Equal(tt.want.Name, got.Name) + } + + if tt.want.Description == "" { + got := got.clone() + dbassert.IsNull(got, "description") + } else { + assert.Equal(tt.want.Description, got.Description) + } + + assert.Equal(tt.want.Name, got.Name) + + // Validate only passwordHmac is returned + assert.Empty(got.Password) + assert.Empty(got.CtPassword) + assert.NotEmpty(got.PasswordHmac) + + // Validate hmac + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(err) + hm, err := crypto.HmacSha256(ctx, tt.want.Password, databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(err) + assert.Equal([]byte(hm), got.PasswordHmac) + + if tt.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } +} + +func TestRepository_UpdatePasswordCredentialKeyUpdate(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + require.NoError(err) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + credStore := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + orig, err := repo.CreateUsernamePasswordCredential(ctx, prj.GetPublicId(), &UsernamePasswordCredential{ + UsernamePasswordCredential: &store.UsernamePasswordCredential{ + Username: "user", + Password: []byte("pass"), + StoreId: credStore.PublicId, + }, + }) + require.NoError(err) + + err = kkms.RotateKeys(ctx, prj.GetPublicId()) + require.NoError(err) + + orig.Password = []byte("pass1") // Company policy to change password every 3 months + + got, _, err := repo.UpdateUsernamePasswordCredential(ctx, prj.GetPublicId(), orig, orig.GetVersion(), []string{"Password"}) + require.NoError(err) + + // Validate that the KeyId has changed + assert.NotEqual(orig.KeyId, got.KeyId) + + // Validate hmac + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.KeyId)) + require.NoError(err) + hm, err := crypto.HmacSha256(ctx, orig.Password, databaseWrapper, []byte(credStore.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(err) + assert.Equal([]byte(hm), got.PasswordHmac) +} + +func TestRepository_UpdateSshPrivateKeyCredential(t *testing.T) { + const testSecondarySshPrivateKeyPem = ` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQAAAJDmpbfr5qW3 +6wAAAAtzc2gtZWQyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQ +AAAEBvvkQkH06ad2GpX1VVARzu9NkHA6gzamAaQ/hkn5FuZvF/CEQBkquextDE6NUDc8WI +Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= +-----END OPENSSH PRIVATE KEY----- +` + + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + changeName := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.Name = n + return c + } + } + + changeDescription := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.Description = d + return c + } + } + + makeNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential { + return nil + } + } + + makeEmbeddedNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential { + return &SshPrivateKeyCredential{} + } + } + + setPublicId := func(n string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.PublicId = n + return c + } + } + + deleteStoreId := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.StoreId = "" + return c + } + } + + deleteVersion := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.Version = 0 + return c + } + } + + changeUser := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.Username = n + return c + } + } + + changePrivateKey := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.PrivateKey = []byte(d) + return c + } + } + + changePrivateKeyPassphrase := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.PrivateKeyPassphrase = []byte(d) + return c + } + } + + combine := func(fns ...func(cs *SshPrivateKeyCredential) *SshPrivateKeyCredential) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + for _, fn := range fns { + c = fn(c) + } + return c + } + } + + tests := []struct { + name string + orig *SshPrivateKeyCredential + chgFn func(*SshPrivateKeyCredential) *SshPrivateKeyCredential + masks []string + want *SshPrivateKeyCredential + wantCount int + wantErr errors.Code + }{ + { + name: "nil-credential", + orig: &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + Username: "user", + PrivateKey: []byte(TestSshPrivateKeyPem), + }, + }, + chgFn: makeNil(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-credential", + orig: &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + Username: "user", + PrivateKey: []byte(TestSshPrivateKeyPem), + }, + }, + chgFn: makeEmbeddedNil(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-public-id", + orig: &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + Username: "user", + PrivateKey: []byte(TestSshPrivateKeyPem), + }, + }, + chgFn: setPublicId(""), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidPublicId, + }, + { + name: "no-store-id", + orig: &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + Username: "user", + PrivateKey: []byte(TestSshPrivateKeyPem), + }, + }, + chgFn: deleteStoreId(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-version", + orig: &SshPrivateKeyCredential{ SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ Username: "user", PrivateKey: []byte(TestSshPrivateKeyPem), @@ -3407,6 +4006,7 @@ func TestRepository_ListDeletedCredentialIds(t *testing.T) { sshCreds := TestSshPrivateKeyCredentials(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), 2) pwCreds := TestUsernamePasswordCredentials(t, conn, wrapper, "username", "testpassword", store.GetPublicId(), prj.GetPublicId(), 2) updCreds := TestUsernamePasswordDomainCredentials(t, conn, wrapper, "username", "testpassword", "domain", store.GetPublicId(), prj.GetPublicId(), 2) + pCreds := TestPasswordCredentials(t, conn, wrapper, "testpassword", store.GetPublicId(), prj.GetPublicId(), 2) repo, err := NewRepository(ctx, rw, rw, kms) require.NoError(err) @@ -3490,6 +4090,23 @@ func TestRepository_ListDeletedCredentialIds(t *testing.T) { require.True(time.Now().Before(ttime.Add(10 * time.Second))) require.True(time.Now().After(ttime.Add(-10 * time.Second))) + // Delete a p credential + _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), pCreds[0].GetPublicId()) + require.NoError(err) + + // Expect five entries + deletedIds, ttime, err = repo.ListDeletedCredentialIds(ctx, time.Now().AddDate(-1, 0, 0)) + require.NoError(err) + assert.Empty( + cmp.Diff( + []string{jsonCreds[0].GetPublicId(), sshCreds[0].GetPublicId(), pwCreds[0].GetPublicId(), updCreds[0].GetPublicId(), pCreds[0].GetPublicId()}, + deletedIds, + cmpopts.SortSlices(func(i, j string) bool { return i < j }), + ), + ) + require.True(time.Now().Before(ttime.Add(10 * time.Second))) + require.True(time.Now().After(ttime.Add(-10 * time.Second))) + // Try again with the time set to now, expect no entries deletedIds, ttime, err = repo.ListDeletedCredentialIds(ctx, time.Now()) require.NoError(err) @@ -3529,13 +4146,14 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { sshCreds := TestSshPrivateKeyCredentials(t, conn, wrapper, "username", TestSshPrivateKeyPem, staticStore.GetPublicId(), prj.GetPublicId(), 2) pwCreds := TestUsernamePasswordCredentials(t, conn, wrapper, "username", "testpassword", staticStore.GetPublicId(), prj.GetPublicId(), 2) updCreds := TestUsernamePasswordDomainCredentials(t, conn, wrapper, "username", "testpassword", "domain", staticStore.GetPublicId(), prj.GetPublicId(), 2) + pCreds := TestPasswordCredentials(t, conn, wrapper, "testpassword", staticStore.GetPublicId(), prj.GetPublicId(), 2) // Run analyze to update postgres meta tables _, err = sqlDb.ExecContext(ctx, "analyze") require.NoError(err) numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(8, numItems) + assert.Equal(10, numItems) // Delete a json credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), jsonCreds[0].GetPublicId()) @@ -3545,7 +4163,7 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(7, numItems) + assert.Equal(9, numItems) // Delete a ssh credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), sshCreds[0].GetPublicId()) @@ -3555,7 +4173,7 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(6, numItems) + assert.Equal(8, numItems) // Delete a pw credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), pwCreds[0].GetPublicId()) @@ -3565,7 +4183,7 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(5, numItems) + assert.Equal(7, numItems) // Delete a upd credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), updCreds[0].GetPublicId()) @@ -3575,5 +4193,15 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(4, numItems) + assert.Equal(6, numItems) + + // Delete a p credential + _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), pCreds[0].GetPublicId()) + require.NoError(err) + _, err = sqlDb.ExecContext(ctx, "analyze") + require.NoError(err) + + numItems, err = repo.EstimatedCredentialCount(ctx) + require.NoError(err) + assert.Equal(5, numItems) } diff --git a/internal/credential/static/repository_credentials.go b/internal/credential/static/repository_credentials.go index 727cbdba36..7c39fd10a9 100644 --- a/internal/credential/static/repository_credentials.go +++ b/internal/credential/static/repository_credentials.go @@ -30,6 +30,11 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin if err != nil { return nil, errors.Wrap(ctx, err, op) } + var pCreds []*PasswordCredential + err = r.reader.SearchWhere(ctx, &pCreds, "public_id in (?)", []any{ids}) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } var spkCreds []*SshPrivateKeyCredential err = r.reader.SearchWhere(ctx, &spkCreds, "public_id in (?)", []any{ids}) if err != nil { @@ -41,9 +46,9 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin return nil, errors.Wrap(ctx, err, op) } - if len(upCreds)+len(updCreds)+len(spkCreds)+len(jsonCreds) != len(ids) { + if len(upCreds)+len(updCreds)+len(pCreds)+len(spkCreds)+len(jsonCreds) != 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(upCreds)+len(spkCreds)+len(jsonCreds))) + fmt.Sprintf("mismatch between creds and number of ids requested, expected %d got %d", len(ids), len(upCreds)+len(updCreds)+len(pCreds)+len(spkCreds)+len(jsonCreds))) } out := make([]credential.Static, 0, len(ids)) @@ -73,6 +78,19 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin out = append(out, c) } + for _, c := range pCreds { + // decrypt credential + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, 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, projectId, kms.KeyPurposeDatabase) diff --git a/internal/credential/static/repository_credentials_test.go b/internal/credential/static/repository_credentials_test.go index 3e0bd358d1..aeae0d342c 100644 --- a/internal/credential/static/repository_credentials_test.go +++ b/internal/credential/static/repository_credentials_test.go @@ -39,6 +39,8 @@ func TestRepository_Retrieve(t *testing.T) { upCred2 := TestUsernamePasswordCredential(t, conn, wrapper, "different user", "better password", staticStore.GetPublicId(), prj.GetPublicId()) updCred1 := TestUsernamePasswordDomainCredential(t, conn, wrapper, "user", "pass", "domain.com", staticStore.GetPublicId(), prj.GetPublicId()) updCred2 := TestUsernamePasswordDomainCredential(t, conn, wrapper, "different user", "better password", "new-domain.com", staticStore.GetPublicId(), prj.GetPublicId()) + pCred1 := TestPasswordCredential(t, conn, wrapper, "better password", staticStore.GetPublicId(), prj.GetPublicId()) + pCred2 := TestPasswordCredential(t, conn, wrapper, "another 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()) spkCredWithPass := TestSshPrivateKeyCredential(t, conn, wrapper, "another last user", @@ -125,6 +127,26 @@ func TestRepository_Retrieve(t *testing.T) { updCred1, updCred2, }, }, + { + name: "valid-one-p-cred", + args: args{ + projectId: prj.GetPublicId(), + credIds: []string{pCred1.GetPublicId()}, + }, + wantCreds: []credential.Static{ + pCred1, + }, + }, + { + name: "valid-multiple-p-creds", + args: args{ + projectId: prj.GetPublicId(), + credIds: []string{pCred1.GetPublicId(), pCred2.GetPublicId()}, + }, + wantCreds: []credential.Static{ + pCred1, pCred2, + }, + }, { name: "valid-ssh-pk-cred", args: args{ @@ -169,10 +191,10 @@ func TestRepository_Retrieve(t *testing.T) { name: "valid-mixed-creds", args: args{ projectId: prj.GetPublicId(), - credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCredWithPass.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId(), jsonCred1.GetPublicId(), jsonCred2.GetPublicId(), updCred1.GetPublicId(), updCred2.GetPublicId()}, + credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCredWithPass.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId(), jsonCred1.GetPublicId(), jsonCred2.GetPublicId(), updCred1.GetPublicId(), updCred2.GetPublicId(), pCred1.GetPublicId(), pCred2.GetPublicId()}, }, wantCreds: []credential.Static{ - upCred1, spkCred1, spkCredWithPass, spkCred2, upCred2, jsonCred1, jsonCred2, updCred1, updCred2, + upCred1, spkCred1, spkCredWithPass, spkCred2, upCred2, jsonCred1, jsonCred2, updCred1, updCred2, pCred1, pCred2, }, }, } @@ -193,6 +215,7 @@ func TestRepository_Retrieve(t *testing.T) { cmpopts.IgnoreUnexported( UsernamePasswordCredential{}, store.UsernamePasswordCredential{}, UsernamePasswordDomainCredential{}, store.UsernamePasswordDomainCredential{}, + PasswordCredential{}, store.PasswordCredential{}, SshPrivateKeyCredential{}, store.SshPrivateKeyCredential{}, JsonCredential{}, store.JsonCredential{}), cmpopts.IgnoreTypes(×tamp.Timestamp{}), diff --git a/internal/credential/static/rewrapping.go b/internal/credential/static/rewrapping.go index d8f2784801..3d26cdb4fe 100644 --- a/internal/credential/static/rewrapping.go +++ b/internal/credential/static/rewrapping.go @@ -17,6 +17,7 @@ func init() { kms.RegisterTableRewrapFn("credential_static_ssh_private_key_credential", credStaticSshPrivKeyRewrapFn) kms.RegisterTableRewrapFn("credential_static_json_credential", credStaticJsonRewrapFn) kms.RegisterTableRewrapFn("credential_static_username_password_domain_credential", credStaticUsernamePasswordDomainRewrapFn) + kms.RegisterTableRewrapFn("credential_static_password_credential", credStaticPasswordRewrapFn) } func rewrapParameterChecks(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) string { @@ -128,6 +129,51 @@ func credStaticUsernamePasswordDomainRewrapFn(ctx context.Context, dataKeyVersio return nil } +func credStaticPasswordRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error { + const op = "static.credStaticPasswordRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var creds []*PasswordCredential + // Indexes exist on (store_id, etc), so we can query static stores via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + rows, err := reader.Query(ctx, credStaticPasswordRewrapQuery, []any{scopeId, dataKeyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + defer rows.Close() + for rows.Next() { + cred := allocPasswordCredential() + if err := rows.Scan( + &cred.PublicId, + &cred.CtPassword, + &cred.KeyId, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to failed to scan row")) + } + creds = append(creds, cred) + } + if err := rows.Err(); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to iterate over retrieved rows")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cred := range creds { + if err := cred.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt password credential")) + } + if err := cred.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt password credential")) + } + if _, err := writer.Update(ctx, cred, []string{"CtPassword", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update password credential row with rewrapped fields")) + } + } + return nil +} + func credStaticSshPrivKeyRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error { const op = "static.credStaticSshPrivKeyRewrapFn" if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { diff --git a/internal/credential/static/rewrapping_test.go b/internal/credential/static/rewrapping_test.go index 0fa4d43931..da68bcef95 100644 --- a/internal/credential/static/rewrapping_test.go +++ b/internal/credential/static/rewrapping_test.go @@ -321,3 +321,67 @@ func TestRewrap_credStaticUsernamePasswordDomainRewrapFn(t *testing.T) { assert.Equal(t, cred.GetPasswordHmac(), got.GetPasswordHmac()) }) } + +func TestRewrap_credStaticPasswordRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT \$1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + mock.ExpectQuery( + `SELECT \* FROM "kms_oplog_schema_version" WHERE 1=1 ORDER BY "kms_oplog_schema_version"."version" LIMIT \$1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `select distinct pass\.public_id, pass\.password_encrypted, pass\.key_id from credential_static_password_credential pass inner join credential_static_store store on store\.public_id = pass\.store_id where store\.project_id = \$1 and pass\.key_id = \$2;`, + ).WillReturnError(errors.New("Query error")) + err := credStaticPasswordRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + cred, err := NewPasswordCredential(cs.GetPublicId(), "password") + assert.NoError(t, err) + + cred.PublicId, err = credential.NewPasswordCredentialId(ctx) + assert.NoError(t, err) + + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + + assert.NoError(t, cred.encrypt(ctx, kmsWrapper)) + assert.NoError(t, rw.Create(context.Background(), cred)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + assert.NoError(t, credStaticPasswordRewrapFn(ctx, cred.GetKeyId(), prj.PublicId, rw, rw, kmsCache)) + + // now we pull the credential back from the db, decrypt it with the new key, and ensure things match + got := allocPasswordCredential() + got.PublicId = cred.PublicId + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, cred.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, "password", string(got.GetPassword())) + assert.NotEmpty(t, got.GetPasswordHmac()) + assert.Equal(t, cred.GetPasswordHmac(), got.GetPasswordHmac()) + }) +} diff --git a/internal/credential/static/store/static.pb.go b/internal/credential/static/store/static.pb.go index d8b368a1d5..0c4b70c191 100644 --- a/internal/credential/static/store/static.pb.go +++ b/internal/credential/static/store/static.pb.go @@ -136,6 +136,157 @@ func (x *CredentialStore) GetVersion() uint32 { return 0 } +type PasswordCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within project_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // store_id of the owning static credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + StoreId string `protobuf:"bytes,6,opt,name=store_id,json=storeId,proto3" json:"store_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // password is the plain-text of the password associated with the credential. We are + // not storing this plain-text password in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,password_data"` + Password []byte `protobuf:"bytes,8,opt,name=password,proto3" json:"password,omitempty" gorm:"-" wrapping:"pt,password_data"` + // ct_password is the ciphertext of the password. It + // is stored in the database. + // @inject_tag: `gorm:"column:password_encrypted;not_null" wrapping:"ct,password_data"` + CtPassword []byte `protobuf:"bytes,9,opt,name=ct_password,json=ctPassword,proto3" json:"ct_password,omitempty" gorm:"column:password_encrypted;not_null" wrapping:"ct,password_data"` + // password_hmac is a sha256-hmac of the unencrypted password. It is recalculated + // everytime the password is updated. + // @inject_tag: `gorm:"not_null"` + PasswordHmac []byte `protobuf:"bytes,10,opt,name=password_hmac,json=passwordHmac,proto3" json:"password_hmac,omitempty" gorm:"not_null"` + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,11,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PasswordCredential) Reset() { + *x = PasswordCredential{} + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PasswordCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PasswordCredential) ProtoMessage() {} + +func (x *PasswordCredential) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PasswordCredential.ProtoReflect.Descriptor instead. +func (*PasswordCredential) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{1} +} + +func (x *PasswordCredential) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *PasswordCredential) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *PasswordCredential) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *PasswordCredential) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PasswordCredential) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *PasswordCredential) GetStoreId() string { + if x != nil { + return x.StoreId + } + return "" +} + +func (x *PasswordCredential) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *PasswordCredential) GetPassword() []byte { + if x != nil { + return x.Password + } + return nil +} + +func (x *PasswordCredential) GetCtPassword() []byte { + if x != nil { + return x.CtPassword + } + return nil +} + +func (x *PasswordCredential) GetPasswordHmac() []byte { + if x != nil { + return x.PasswordHmac + } + return nil +} + +func (x *PasswordCredential) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + type UsernamePasswordCredential struct { state protoimpl.MessageState `protogen:"open.v1"` // public_id is a surrogate key suitable for use in a public API. @@ -186,7 +337,7 @@ type UsernamePasswordCredential struct { func (x *UsernamePasswordCredential) Reset() { *x = UsernamePasswordCredential{} - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[1] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -198,7 +349,7 @@ func (x *UsernamePasswordCredential) String() string { func (*UsernamePasswordCredential) ProtoMessage() {} func (x *UsernamePasswordCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[1] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -211,7 +362,7 @@ func (x *UsernamePasswordCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use UsernamePasswordCredential.ProtoReflect.Descriptor instead. func (*UsernamePasswordCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{1} + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{2} } func (x *UsernamePasswordCredential) GetPublicId() string { @@ -352,7 +503,7 @@ type UsernamePasswordDomainCredential struct { func (x *UsernamePasswordDomainCredential) Reset() { *x = UsernamePasswordDomainCredential{} - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -364,7 +515,7 @@ func (x *UsernamePasswordDomainCredential) String() string { func (*UsernamePasswordDomainCredential) ProtoMessage() {} func (x *UsernamePasswordDomainCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -377,7 +528,7 @@ func (x *UsernamePasswordDomainCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use UsernamePasswordDomainCredential.ProtoReflect.Descriptor instead. func (*UsernamePasswordDomainCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{2} + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{3} } func (x *UsernamePasswordDomainCredential) GetPublicId() string { @@ -534,7 +685,7 @@ type SshPrivateKeyCredential struct { func (x *SshPrivateKeyCredential) Reset() { *x = SshPrivateKeyCredential{} - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -546,7 +697,7 @@ func (x *SshPrivateKeyCredential) String() string { func (*SshPrivateKeyCredential) ProtoMessage() {} func (x *SshPrivateKeyCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -559,7 +710,7 @@ func (x *SshPrivateKeyCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use SshPrivateKeyCredential.ProtoReflect.Descriptor instead. func (*SshPrivateKeyCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{3} + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{4} } func (x *SshPrivateKeyCredential) GetPublicId() string { @@ -713,7 +864,7 @@ type JsonCredential struct { func (x *JsonCredential) Reset() { *x = JsonCredential{} - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[4] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -725,7 +876,7 @@ func (x *JsonCredential) String() string { func (*JsonCredential) ProtoMessage() {} func (x *JsonCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[4] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -738,7 +889,7 @@ func (x *JsonCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use JsonCredential.ProtoReflect.Descriptor instead. func (*JsonCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{4} + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{5} } func (x *JsonCredential) GetPublicId() string { @@ -835,7 +986,27 @@ const file_controller_storage_credential_static_store_v1_static_proto_rawDesc = "\vDescription\x12\vdescriptionR\vdescription\x12\x1d\n" + "\n" + "project_id\x18\x06 \x01(\tR\tprojectId\x12\x18\n" + - "\aversion\x18\a \x01(\rR\aversion\"\xfd\x04\n" + + "\aversion\x18\a \x01(\rR\aversion\"\xb4\x04\n" + + "\x12PasswordCredential\x12\x1b\n" + + "\tpublic_id\x18\x01 \x01(\tR\bpublicId\x12K\n" + + "\vcreate_time\x18\x02 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" + + "createTime\x12K\n" + + "\vupdate_time\x18\x03 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" + + "updateTime\x12$\n" + + "\x04name\x18\x04 \x01(\tB\x10\xc2\xdd)\f\n" + + "\x04Name\x12\x04nameR\x04name\x12@\n" + + "\vdescription\x18\x05 \x01(\tB\x1e\xc2\xdd)\x1a\n" + + "\vDescription\x12\vdescriptionR\vdescription\x12\x19\n" + + "\bstore_id\x18\x06 \x01(\tR\astoreId\x12\x18\n" + + "\aversion\x18\a \x01(\rR\aversion\x12?\n" + + "\bpassword\x18\b \x01(\fB#\xc2\xdd)\x1f\n" + + "\bPassword\x12\x13attributes.passwordR\bpassword\x12\x1f\n" + + "\vct_password\x18\t \x01(\fR\n" + + "ctPassword\x12Q\n" + + "\rpassword_hmac\x18\n" + + " \x01(\fB,\xc2\xdd)(\n" + + "\fPasswordHmac\x12\x18attributes.password_hmacR\fpasswordHmac\x12\x15\n" + + "\x06key_id\x18\v \x01(\tR\x05keyId\"\xfd\x04\n" + "\x1aUsernamePasswordCredential\x12\x1b\n" + "\tpublic_id\x18\x01 \x01(\tR\bpublicId\x12K\n" + "\vcreate_time\x18\x02 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" + @@ -944,31 +1115,34 @@ func file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP return file_controller_storage_credential_static_store_v1_static_proto_rawDescData } -var file_controller_storage_credential_static_store_v1_static_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_controller_storage_credential_static_store_v1_static_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_controller_storage_credential_static_store_v1_static_proto_goTypes = []any{ (*CredentialStore)(nil), // 0: controller.storage.credential.static.store.v1.CredentialStore - (*UsernamePasswordCredential)(nil), // 1: controller.storage.credential.static.store.v1.UsernamePasswordCredential - (*UsernamePasswordDomainCredential)(nil), // 2: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential - (*SshPrivateKeyCredential)(nil), // 3: controller.storage.credential.static.store.v1.SshPrivateKeyCredential - (*JsonCredential)(nil), // 4: controller.storage.credential.static.store.v1.JsonCredential - (*timestamp.Timestamp)(nil), // 5: controller.storage.timestamp.v1.Timestamp + (*PasswordCredential)(nil), // 1: controller.storage.credential.static.store.v1.PasswordCredential + (*UsernamePasswordCredential)(nil), // 2: controller.storage.credential.static.store.v1.UsernamePasswordCredential + (*UsernamePasswordDomainCredential)(nil), // 3: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential + (*SshPrivateKeyCredential)(nil), // 4: controller.storage.credential.static.store.v1.SshPrivateKeyCredential + (*JsonCredential)(nil), // 5: controller.storage.credential.static.store.v1.JsonCredential + (*timestamp.Timestamp)(nil), // 6: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_static_store_v1_static_proto_depIdxs = []int32{ - 5, // 0: controller.storage.credential.static.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 1: controller.storage.credential.static.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 2: controller.storage.credential.static.store.v1.UsernamePasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 3: controller.storage.credential.static.store.v1.UsernamePasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 4: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 5: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 6: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 7: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 8: controller.storage.credential.static.store.v1.JsonCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 9: controller.storage.credential.static.store.v1.JsonCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 6, // 0: controller.storage.credential.static.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 1: controller.storage.credential.static.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 2: controller.storage.credential.static.store.v1.PasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 3: controller.storage.credential.static.store.v1.PasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 4: controller.storage.credential.static.store.v1.UsernamePasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 5: controller.storage.credential.static.store.v1.UsernamePasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 6: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 7: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 8: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 9: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 10: controller.storage.credential.static.store.v1.JsonCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 11: controller.storage.credential.static.store.v1.JsonCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_controller_storage_credential_static_store_v1_static_proto_init() } @@ -982,7 +1156,7 @@ func file_controller_storage_credential_static_store_v1_static_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_storage_credential_static_store_v1_static_proto_rawDesc), len(file_controller_storage_credential_static_store_v1_static_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/credential/static/testing.go b/internal/credential/static/testing.go index 506fce34b1..1f472d81cb 100644 --- a/internal/credential/static/testing.go +++ b/internal/credential/static/testing.go @@ -284,6 +284,77 @@ func TestUsernamePasswordDomainCredentials( return creds } +// TestPasswordCredential creates a password credential in the provided DB with +// the provided project id and any values passed in through. +// If any errors are encountered during the creation of the store, the test will fail. +func TestPasswordCredential( + t testing.TB, + conn *db.DB, + wrapper wrapping.Wrapper, + password, storeId, projectId string, + opts ...Option, +) *PasswordCredential { + t.Helper() + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + w := db.New(conn) + + opt := getOpts(opts...) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + require.NotNil(t, databaseWrapper) + + cred, err := NewPasswordCredential(storeId, credential.Password(password), opts...) + require.NoError(t, err) + require.NotNil(t, cred) + + id := opt.withPublicId + if id == "" { + id, err = credential.NewPasswordCredentialId(ctx) + require.NoError(t, err) + } + cred.PublicId = id + + err = cred.encrypt(ctx, databaseWrapper) + require.NoError(t, err) + + _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + require.NoError(t, iw.Create(ctx, cred)) + return nil + }, + ) + require.NoError(t, err2) + + return cred +} + +// TestPasswordCredentials creates count number of password credentials in +// the provided DB with the provided project id. If any errors are +// encountered during the creation of the credentials, the test will fail. +func TestPasswordCredentials( + t testing.TB, + conn *db.DB, + wrapper wrapping.Wrapper, + password, storeId, projectId string, + count int, +) []*PasswordCredential { + t.Helper() + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + require.NotNil(t, databaseWrapper) + + creds := make([]*PasswordCredential, 0, count) + for i := 0; i < count; i++ { + creds = append(creds, TestPasswordCredential(t, conn, wrapper, password, storeId, projectId)) + } + return creds +} + // TestSshPrivateKeyCredential creates an ssh private key credential in the // provided DB with the provided project and any values passed in through. If any // errors are encountered during the creation of the store, the test will fail. diff --git a/internal/proto/controller/storage/credential/static/store/v1/static.proto b/internal/proto/controller/storage/credential/static/store/v1/static.proto index 0a2ea01ad4..49c60a5fc3 100644 --- a/internal/proto/controller/storage/credential/static/store/v1/static.proto +++ b/internal/proto/controller/storage/credential/static/store/v1/static.proto @@ -49,6 +49,69 @@ message CredentialStore { uint32 version = 7; } +message PasswordCredential { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 1; + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // name is optional. If set, it must be unique within project_id. + // @inject_tag: `gorm:"default:null"` + string name = 4 [(custom_options.v1.mask_mapping) = { + this: "Name" + that: "name" + }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 5 [(custom_options.v1.mask_mapping) = { + this: "Description" + that: "description" + }]; + + // store_id of the owning static credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string store_id = 6; + + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + uint32 version = 7; + + // password is the plain-text of the password associated with the credential. We are + // not storing this plain-text password in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,password_data"` + bytes password = 8 [(custom_options.v1.mask_mapping) = { + this: "Password" + that: "attributes.password" + }]; + + // ct_password is the ciphertext of the password. It + // is stored in the database. + // @inject_tag: `gorm:"column:password_encrypted;not_null" wrapping:"ct,password_data"` + bytes ct_password = 9; + + // password_hmac is a sha256-hmac of the unencrypted password. It is recalculated + // everytime the password is updated. + // @inject_tag: `gorm:"not_null"` + bytes password_hmac = 10 [(custom_options.v1.mask_mapping) = { + this: "PasswordHmac" + that: "attributes.password_hmac" + }]; + + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string key_id = 11; +} + message UsernamePasswordCredential { // public_id is a surrogate key suitable for use in a public API. // @inject_tag: `gorm:"primary_key"`