From 34b4028d318e3a6f9c501f176f122ffc76efda9c Mon Sep 17 00:00:00 2001 From: April-May <18632637+AprilMay0@users.noreply.github.com> Date: Thu, 22 May 2025 20:00:26 +0100 Subject: [PATCH] feat(credential/static): Implement CRUDL for UsernamePasswordDomain Co-authored-by: Hugo <10965479+hugoghx@users.noreply.github.com> Co-authored-by: Andrew Gaffney <109917432+anGaffney@users.noreply.github.com> --- internal/credential/static/credential.go | 21 + internal/credential/static/fields.go | 1 + internal/credential/static/query.go | 97 ++ .../static/repository_credential.go | 231 +++++ .../static/repository_credential_test.go | 960 +++++++++++++++++- .../static/repository_credentials.go | 20 +- .../static/repository_credentials_test.go | 27 +- internal/credential/static/rewrapping_test.go | 3 +- internal/credential/static/testing.go | 71 ++ .../usernamepassworddomain_credential.go | 3 +- .../usernamepassworddomain_credential_test.go | 3 +- 11 files changed, 1417 insertions(+), 20 deletions(-) diff --git a/internal/credential/static/credential.go b/internal/credential/static/credential.go index 1ba4952a9..fd7665239 100644 --- a/internal/credential/static/credential.go +++ b/internal/credential/static/credential.go @@ -22,6 +22,7 @@ type listCredentialResult struct { Name string Description string Username string + Domain string KeyId string Hmac1 string Hmac2 string @@ -71,6 +72,26 @@ func (c *listCredentialResult) toCredential(ctx context.Context) (credential.Sta cred.PasswordHmac = []byte(c.Hmac1) } return cred, nil + case "upd": + cred := &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + PublicId: c.PublicId, + StoreId: c.StoreId, + Name: c.Name, + Description: c.Description, + CreateTime: c.CreateTime, + UpdateTime: c.UpdateTime, + Version: uint32(c.Version), + Username: c.Username, + Domain: c.Domain, + 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/fields.go b/internal/credential/static/fields.go index b80665387..857b66800 100644 --- a/internal/credential/static/fields.go +++ b/internal/credential/static/fields.go @@ -9,6 +9,7 @@ const ( descriptionField = "Description" usernameField = "Username" passwordField = "Password" + domainField = "Domain" privateKeyField = "PrivateKey" PrivateKeyPassphraseField = "PrivateKeyPassphrase" objectField = "Object" diff --git a/internal/credential/static/query.go b/internal/credential/static/query.go index 3f9886e6c..9fbbab6fa 100644 --- a/internal/credential/static/query.go +++ b/internal/credential/static/query.go @@ -55,6 +55,7 @@ select sum(reltuples::bigint) as estimate where oid in ( 'credential_static_json_credential'::regclass, 'credential_static_username_password_credential'::regclass, + 'credential_static_username_password_domain_credential'::regclass, 'credential_static_ssh_private_key_credential'::regclass ) ` @@ -77,6 +78,11 @@ upw_creds as ( from credential_static_username_password_credential where public_id in (select public_id from credentials) ), +upd_creds as ( + select * + from credential_static_username_password_domain_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -92,6 +98,7 @@ final as ( 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, object_hmac as hmac1, null::bytea as hmac2, -- Add this to make the union uniform @@ -107,11 +114,28 @@ final as ( update_time, version, username, + 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 'upw' as type from upw_creds + union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + username, + domain, + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'upd' as type + from upd_creds union select public_id, store_id, @@ -122,6 +146,7 @@ final as ( update_time, version, username, + null as domain, -- Add this to make the union uniform key_id, private_key_hmac as hmac1, private_key_passphrase_hmac as hmac2, @@ -152,6 +177,11 @@ upw_creds as ( from credential_static_username_password_credential where public_id in (select public_id from credentials) ), +upd_creds as ( + select * + from credential_static_username_password_domain_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -167,6 +197,7 @@ final as ( 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, object_hmac as hmac1, null::bytea as hmac2, -- Add this to make the union uniform @@ -182,6 +213,7 @@ final as ( update_time, version, username, + 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 @@ -197,6 +229,23 @@ final as ( update_time, version, username, + domain, + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'upd' as type + from upd_creds + union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + username, + null as domain, -- Add this to make the union uniform key_id, private_key_hmac as hmac1, private_key_passphrase_hmac as hmac2, @@ -227,6 +276,11 @@ upw_creds as ( from credential_static_username_password_credential where public_id in (select public_id from credentials) ), +upd_creds as ( + select * + from credential_static_username_password_domain_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -242,6 +296,7 @@ final as ( 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, object_hmac as hmac1, null::bytea as hmac2, -- Add this to make the union uniform @@ -257,6 +312,7 @@ final as ( update_time, version, username, + 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 @@ -272,6 +328,23 @@ final as ( update_time, version, username, + domain, + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'upd' as type + from upd_creds + union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + username, + null as domain, -- Add this to make the union uniform key_id, private_key_hmac as hmac1, private_key_passphrase_hmac as hmac2, @@ -303,6 +376,11 @@ upw_creds as ( from credential_static_username_password_credential where public_id in (select public_id from credentials) ), +upd_creds as ( + select * + from credential_static_username_password_domain_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -318,6 +396,7 @@ final as ( 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, object_hmac as hmac1, null::bytea as hmac2, -- Add this to make the union uniform @@ -333,6 +412,7 @@ final as ( update_time, version, username, + 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 @@ -348,6 +428,23 @@ final as ( update_time, version, username, + domain, + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'upd' as type + from upd_creds + union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + username, + null as domain, -- Add this to make the union uniform key_id, private_key_hmac as hmac1, private_key_passphrase_hmac as hmac2, diff --git a/internal/credential/static/repository_credential.go b/internal/credential/static/repository_credential.go index deaa1866f..4b851ec5a 100644 --- a/internal/credential/static/repository_credential.go +++ b/internal/credential/static/repository_credential.go @@ -107,6 +107,95 @@ func (r *Repository) CreateUsernamePasswordCredential( return newCred, nil } +// CreateUsernamePasswordDomainCredential inserts c into the repository and returns a new +// UsernamePasswordDomainCredential 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) CreateUsernamePasswordDomainCredential( + ctx context.Context, + projectId string, + c *UsernamePasswordDomainCredential, + _ ...Option, +) (*UsernamePasswordDomainCredential, error) { + const op = "static.(Repository).CreateUsernamePasswordDomainCredential" + if c == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing credential") + } + if c.UsernamePasswordDomainCredential == 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.Username == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing username") + } + if c.Domain == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing domain") + } + 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.NewUsernamePasswordDomainCredentialId(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 *UsernamePasswordDomainCredential + _, 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 @@ -308,6 +397,20 @@ func (r *Repository) LookupCredential(ctx context.Context, publicId string, _ .. upCred.Password = nil cred = upCred + case credential.UsernamePasswordDomainSubtype: + updCred := allocUsernamePasswordDomainCredential() + updCred.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, updCred); 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 + updCred.CtPassword = nil + updCred.Password = nil + cred = updCred + case credential.SshPrivateKeySubtype: spkCred := allocSshPrivateKeyCredential() spkCred.PublicId = publicId @@ -457,6 +560,122 @@ func (r *Repository) UpdateUsernamePasswordCredential(ctx context.Context, return returnedCredential, rowsUpdated, nil } +// UpdateUsernamePasswordDomainCredential updates the repository entry for c.PublicId with +// the values in c for the fields listed in fieldMaskPaths. It returns a +// new UsernamePasswordDomainCredential 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, Username, Password, and Domain 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) UpdateUsernamePasswordDomainCredential(ctx context.Context, + projectId string, + c *UsernamePasswordDomainCredential, + version uint32, + fieldMaskPaths []string, + _ ...Option, +) (*UsernamePasswordDomainCredential, int, error) { + const op = "static.(Repository).UpdateUsernamePasswordDomainCredential" + if c == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing credential") + } + if c.UsernamePasswordDomainCredential == 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(usernameField, f): + case strings.EqualFold(passwordField, f): + case strings.EqualFold(domainField, 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, + usernameField: c.Username, + passwordField: c.Password, + domainField: c.Domain, + }, + 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 *UsernamePasswordDomainCredential + _, 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 @@ -865,6 +1084,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.UsernamePasswordDomainSubtype: + c := allocUsernamePasswordDomainCredential() + c.PublicId = id + input = c + md = c.oplog(oplog.OpType_OP_TYPE_DELETE) case credential.SshPrivateKeySubtype: c := allocSshPrivateKeyCredential() c.PublicId = id @@ -946,6 +1170,13 @@ func (r *Repository) ListDeletedCredentialIds(ctx context.Context, since time.Ti for _, cl := range deletedUsernamePasswordCredentials { credentialStoreIds = append(credentialStoreIds, cl.PublicId) } + var deletedUsernamePasswordDomainCredentials []*deletedUsernamePasswordDomainCredential + if err := r.SearchWhere(ctx, &deletedUsernamePasswordDomainCredentials, "delete_time >= ?", []any{since}); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted username password domain credentials")) + } + for _, cl := range deletedUsernamePasswordDomainCredentials { + 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 cd64e9202..30293e26b 100644 --- a/internal/credential/static/repository_credential_test.go +++ b/internal/credential/static/repository_credential_test.go @@ -200,6 +200,197 @@ func TestRepository_CreateUsernamePasswordCredential(t *testing.T) { }) } +func TestRepository_CreateUsernamePasswordDomainCredential(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 *UsernamePasswordDomainCredential + wantErr bool + wantErrCode errors.Code + }{ + { + name: "missing-store", + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-embedded-cred", + cred: &UsernamePasswordDomainCredential{}, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-project-id", + cred: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "my-user", + Password: []byte("secret"), + Domain: "domain.com", + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-username", + projectId: prj.PublicId, + cred: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Password: []byte("secret"), + Domain: "domain.com", + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-domain", + projectId: prj.PublicId, + cred: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "my-user", + Password: []byte("secret"), + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-password", + projectId: prj.PublicId, + cred: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "my-user", + Domain: "domain.com", + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-store-id", + projectId: prj.PublicId, + cred: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "my-user", + Password: []byte("secret"), + Domain: "domain.com", + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "valid", + projectId: prj.PublicId, + cred: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "my-user", + Password: []byte("secret"), + Domain: "domain.com", + 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.CreateUsernamePasswordDomainCredential(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.UsernamePasswordDomainCredentialPrefix, got.PublicId) + assert.Equal(tt.cred.Username, got.Username) + assert.Nil(got.Password) + assert.Nil(got.CtPassword) + + // Validate password + lookupCred := allocUsernamePasswordDomainCredential() + 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 := NewUsernamePasswordDomainCredential(prjCs.GetPublicId(), "user", "pass", "domain.com", WithName("my-name"), WithDescription("original")) + assert.NoError(err) + + got, err := repo.CreateUsernamePasswordDomainCredential(ctx, prj.PublicId, in) + require.NoError(err) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + + in2, err := NewUsernamePasswordDomainCredential(prjCs.GetPublicId(), "user", "pass", "domain.com", WithName("my-name"), WithDescription("different")) + require.NoError(err) + got2, err := repo.CreateUsernamePasswordDomainCredential(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 := NewUsernamePasswordDomainCredential(prj2Cs.GetPublicId(), "user", "pass", "domain.com", WithName("my-name"), WithDescription("different")) + require.NoError(err) + got3, err := repo.CreateUsernamePasswordDomainCredential(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") @@ -574,6 +765,7 @@ func TestRepository_LookupCredential(t *testing.T) { _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) 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) 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))) @@ -593,6 +785,11 @@ func TestRepository_LookupCredential(t *testing.T) { id: upCred.GetPublicId(), want: upCred, }, + { + name: "upd-valid", + id: updCred.GetPublicId(), + want: updCred, + }, { name: "spk-valid", id: spkCred.GetPublicId(), @@ -648,6 +845,10 @@ func TestRepository_LookupCredential(t *testing.T) { assert.Empty(v.Password) assert.Empty(v.CtPassword) assert.NotEmpty(v.PasswordHmac) + case *UsernamePasswordDomainCredential: + assert.Empty(v.Password) + assert.Empty(v.CtPassword) + assert.NotEmpty(v.PasswordHmac) case *SshPrivateKeyCredential: assert.Empty(v.PrivateKey) assert.Empty(v.PrivateKeyEncrypted) @@ -678,15 +879,16 @@ func TestRepository_ListCredentials(t *testing.T) { kms := kms.TestKms(t, conn, wrapper) defaultLimit := 5 - total := 30 + total := 40 _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) - TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), total/3) - TestSshPrivateKeyCredentials(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), total/3) + 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) + TestSshPrivateKeyCredentials(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), total/4) obj, _ := TestJsonObject(t) - TestJsonCredentials(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj, total/3) + TestJsonCredentials(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj, total/4) type args struct { storeId string @@ -742,6 +944,10 @@ func TestRepository_ListCredentials(t *testing.T) { assert.Empty(v.Password) assert.Empty(v.CtPassword) assert.NotEmpty(v.PasswordHmac) + case *UsernamePasswordDomainCredential: + assert.Empty(v.Password) + assert.Empty(v.CtPassword) + assert.NotEmpty(v.PasswordHmac) case *SshPrivateKeyCredential: assert.Empty(v.PrivateKey) assert.Empty(v.PrivateKeyEncrypted) @@ -771,7 +977,8 @@ func TestRepository_ListCredentials_Pagination(t *testing.T) { obj, _ := TestJsonObject(t) _ = TestJsonCredentials(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj, 2) _ = TestSshPrivateKeyCredentials(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), 2) - _ = TestUsernamePasswordCredentials(t, conn, wrapper, "username", "testpassword", store.GetPublicId(), prj.GetPublicId(), 1) + _ = TestUsernamePasswordCredentials(t, conn, wrapper, "username", "testpassword", store.GetPublicId(), prj.GetPublicId(), 2) + _ = TestUsernamePasswordDomainCredentials(t, conn, wrapper, "username", "testpassword", "domain.com", store.GetPublicId(), prj.GetPublicId(), 1) repo, err := NewRepository(ctx, rw, rw, kms) require.NoError(err) @@ -783,26 +990,109 @@ func TestRepository_ListCredentials_Pagination(t *testing.T) { // Transaction timestamp should be within ~10 seconds of now assert.True(time.Now().Before(ttime.Add(10 * time.Second))) assert.True(time.Now().After(ttime.Add(-10 * time.Second))) + page2, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page1[1])) require.NoError(err) require.Len(page2, 2) - for _, item := range page1 { + pages := page1 + for _, item := range pages { assert.NotEqual(item.GetPublicId(), page2[0].GetPublicId()) assert.NotEqual(item.GetPublicId(), page2[1].GetPublicId()) } assert.True(time.Now().Before(ttime.Add(10 * time.Second))) assert.True(time.Now().After(ttime.Add(-10 * time.Second))) + page3, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page2[1])) require.NoError(err) - require.Len(page3, 1) - for _, item := range append(page1, page2...) { + require.Len(page3, 2) + pages = append(pages, page2...) + for _, item := range pages { assert.NotEqual(item.GetPublicId(), page3[0].GetPublicId()) + assert.NotEqual(item.GetPublicId(), page3[1].GetPublicId()) } assert.True(time.Now().Before(ttime.Add(10 * time.Second))) assert.True(time.Now().After(ttime.Add(-10 * time.Second))) - page4, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page3[0])) + + page4, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page3[1])) require.NoError(err) - require.Empty(page4) + require.Len(page4, 1) + pages = append(pages, page3...) + for _, item := range pages { + assert.NotEqual(item.GetPublicId(), page4[0].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])) + require.NoError(err) + require.Empty(page5) + assert.True(time.Now().Before(ttime.Add(10 * time.Second))) + assert.True(time.Now().After(ttime.Add(-10 * time.Second))) +} + +func TestRepository_ListCredentialsRefresh(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + currTime := time.Now() + store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + obj, _ := TestJsonObject(t) + _ = TestJsonCredentials(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj, 2) + _ = 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) + + repo, err := NewRepository(ctx, rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + + page1, ttime, err := repo.ListCredentialsRefresh(ctx, store.GetPublicId(), currTime, credential.WithLimit(2)) + require.NoError(err) + require.Len(page1, 2) + // Transaction timestamp should be within ~10 seconds of now + assert.True(time.Now().Before(ttime.Add(10 * time.Second))) + assert.True(time.Now().After(ttime.Add(-10 * time.Second))) + + page2, ttime, err := repo.ListCredentialsRefresh(ctx, store.GetPublicId(), currTime, credential.WithLimit(2), credential.WithStartPageAfterItem(page1[1])) + require.NoError(err) + require.Len(page2, 2) + pages := page1 + for _, item := range pages { + assert.NotEqual(item.GetPublicId(), page2[0].GetPublicId()) + assert.NotEqual(item.GetPublicId(), page2[1].GetPublicId()) + } + assert.True(time.Now().Before(ttime.Add(10 * time.Second))) + assert.True(time.Now().After(ttime.Add(-10 * time.Second))) + + page3, ttime, err := repo.ListCredentialsRefresh(ctx, store.GetPublicId(), currTime, credential.WithLimit(2), credential.WithStartPageAfterItem(page2[1])) + require.NoError(err) + require.Len(page3, 2) + pages = append(pages, page2...) + for _, item := range pages { + assert.NotEqual(item.GetPublicId(), page3[0].GetPublicId()) + assert.NotEqual(item.GetPublicId(), page3[1].GetPublicId()) + } + assert.True(time.Now().Before(ttime.Add(10 * time.Second))) + assert.True(time.Now().After(ttime.Add(-10 * time.Second))) + + page4, ttime, err := repo.ListCredentialsRefresh(ctx, store.GetPublicId(), currTime, credential.WithLimit(2), credential.WithStartPageAfterItem(page3[1])) + require.NoError(err) + require.Len(page4, 1) + pages = append(pages, page3...) + for _, item := range pages { + assert.NotEqual(item.GetPublicId(), page4[0].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.ListCredentialsRefresh(ctx, store.GetPublicId(), currTime, credential.WithLimit(2), credential.WithStartPageAfterItem(page4[0])) + require.NoError(err) + require.Empty(page5) assert.True(time.Now().Before(ttime.Add(10 * time.Second))) assert.True(time.Now().After(ttime.Add(-10 * time.Second))) } @@ -1389,6 +1679,619 @@ func TestRepository_UpdateUsernamePasswordCredential(t *testing.T) { } } +func TestRepository_UpdateUsernamePasswordDomainCredential(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + changeName := func(n string) func(credential *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + c.Name = n + return c + } + } + + changeDescription := func(d string) func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + c.Description = d + return c + } + } + + makeNil := func() func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(_ *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return nil + } + } + + makeEmbeddedNil := func() func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(_ *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return &UsernamePasswordDomainCredential{} + } + } + + setPublicId := func(n string) func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + c.PublicId = n + return c + } + } + + deleteStoreId := func() func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + c.StoreId = "" + return c + } + } + + deleteVersion := func() func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + c.Version = 0 + return c + } + } + + changeUser := func(n string) func(credential *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + c.Username = n + return c + } + } + + changePassword := func(d string) func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + c.Password = []byte(d) + return c + } + } + + changeDomain := func(d string) func(credential *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + c.Domain = d + return c + } + } + + combine := func(fns ...func(cs *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential) func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + return func(c *UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential { + for _, fn := range fns { + c = fn(c) + } + return c + } + } + + tests := []struct { + name string + orig *UsernamePasswordDomainCredential + chgFn func(*UsernamePasswordDomainCredential) *UsernamePasswordDomainCredential + masks []string + want *UsernamePasswordDomainCredential + wantCount int + wantErr errors.Code + }{ + { + name: "nil-credential", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: makeNil(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-credential", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: makeEmbeddedNil(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-public-id", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: setPublicId(""), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidPublicId, + }, + { + name: "no-store-id", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: deleteStoreId(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-version", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: deleteVersion(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "updating-non-existent-credential", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: combine(setPublicId("abcd_OOOOOOOOOO"), changeName("test-update-name-repo")), + masks: []string{"Name"}, + wantErr: errors.RecordNotFound, + }, + { + name: "empty-field-mask", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: changeName("test-update-name-repo"), + wantErr: errors.EmptyFieldMask, + }, + { + name: "read-only-fields-in-field-mask", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "ProjectId"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "unknown-field-in-field-mask", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Bilbo"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "change-name", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Name"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-update-name-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "change-description", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Description: "test-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{"Description"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Description: "test-update-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")), + masks: []string{"Name", "Description"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-update-name-repo", + Description: "test-update-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "change-username", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: changeUser("test-update-user"), + masks: []string{"Username"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "test-update-user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "change-password", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: changePassword("test-update-pass"), + masks: []string{"Password"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("test-update-pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "change-domain", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: changeDomain("test-update-domain.com"), + masks: []string{"Domain"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "test-update-domain.com", + }, + }, + wantCount: 1, + }, + { + name: "change-username-and-domain", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: combine(changeUser("test-update-user"), changeDomain("test-update-domain.com")), + masks: []string{"Username", "Domain"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "test-update-user", + Password: []byte("pass"), + Domain: "test-update-domain.com", + }, + }, + wantCount: 1, + }, + { + name: "change-username-and-password", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: combine(changeUser("test-update-user"), changePassword("test-update-pass")), + masks: []string{"Username", "Password"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "test-update-user", + Password: []byte("test-update-pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "change-username-and-password-and-domain", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + chgFn: combine(changeUser("test-update-user"), changePassword("test-update-pass"), changeDomain("test-update-domain.com")), + masks: []string{"Username", "Password", "Domain"}, + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "test-update-user", + Password: []byte("test-update-pass"), + Domain: "test-update-domain.com", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-password", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + masks: []string{"Username"}, + chgFn: combine(changeUser("test-new-user"), changePassword("")), + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "test-new-user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-username", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + masks: []string{"Password"}, + chgFn: combine(changeUser(""), changePassword("test-new-password")), + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Username: "user", + Password: []byte("test-new-password"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Description: "test-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Description: "test-update-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + orig: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &UsernamePasswordDomainCredential{ + UsernamePasswordDomainCredential: &store.UsernamePasswordDomainCredential{ + Name: "test-update-name-repo", + Description: "test-description-repo", + Username: "user", + Password: []byte("pass"), + Domain: "domain.com", + }, + }, + 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.CreateUsernamePasswordDomainCredential(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.UpdateUsernamePasswordDomainCredential(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.UsernamePasswordDomainCredentialPrefix, 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.Username, got.Username) + + // 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") @@ -2503,6 +3406,7 @@ func TestRepository_ListDeletedCredentialIds(t *testing.T) { jsonCreds := TestJsonCredentials(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj, 2) 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) repo, err := NewRepository(ctx, rw, rw, kms) require.NoError(err) @@ -2569,6 +3473,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 upd credential + _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), updCreds[0].GetPublicId()) + require.NoError(err) + + // Expect four 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()}, + 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) @@ -2607,13 +3528,14 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { jsonCreds := TestJsonCredentials(t, conn, wrapper, staticStore.GetPublicId(), prj.GetPublicId(), obj, 2) 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) // 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(6, numItems) + assert.Equal(8, numItems) // Delete a json credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), jsonCreds[0].GetPublicId()) @@ -2623,7 +3545,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 ssh credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), sshCreds[0].GetPublicId()) @@ -2633,7 +3555,7 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(4, numItems) + assert.Equal(6, numItems) // Delete a pw credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), pwCreds[0].GetPublicId()) @@ -2643,5 +3565,15 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(3, numItems) + assert.Equal(5, numItems) + + // Delete a upd credential + _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), updCreds[0].GetPublicId()) + require.NoError(err) + _, err = sqlDb.ExecContext(ctx, "analyze") + require.NoError(err) + + numItems, err = repo.EstimatedCredentialCount(ctx) + require.NoError(err) + assert.Equal(4, numItems) } diff --git a/internal/credential/static/repository_credentials.go b/internal/credential/static/repository_credentials.go index df2881a59..727cbdba3 100644 --- a/internal/credential/static/repository_credentials.go +++ b/internal/credential/static/repository_credentials.go @@ -25,6 +25,11 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin if err != nil { return nil, errors.Wrap(ctx, err, op) } + var updCreds []*UsernamePasswordDomainCredential + err = r.reader.SearchWhere(ctx, &updCreds, "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 { @@ -36,7 +41,7 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin return nil, errors.Wrap(ctx, err, op) } - if len(upCreds)+len(spkCreds)+len(jsonCreds) != len(ids) { + if len(upCreds)+len(updCreds)+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))) } @@ -55,6 +60,19 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin out = append(out, c) } + for _, c := range updCreds { + // 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 9488da6e1..3e0bd358d 100644 --- a/internal/credential/static/repository_credentials_test.go +++ b/internal/credential/static/repository_credentials_test.go @@ -37,6 +37,8 @@ func TestRepository_Retrieve(t *testing.T) { staticStore := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) upCred1 := TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", staticStore.GetPublicId(), prj.GetPublicId()) upCred2 := TestUsernamePasswordCredential(t, conn, wrapper, "different user", "better password", staticStore.GetPublicId(), prj.GetPublicId()) + 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()) 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", @@ -103,6 +105,26 @@ func TestRepository_Retrieve(t *testing.T) { upCred1, upCred2, }, }, + { + name: "valid-one-upd-cred", + args: args{ + projectId: prj.GetPublicId(), + credIds: []string{updCred1.GetPublicId()}, + }, + wantCreds: []credential.Static{ + updCred1, + }, + }, + { + name: "valid-multiple-upd-creds", + args: args{ + projectId: prj.GetPublicId(), + credIds: []string{updCred1.GetPublicId(), updCred2.GetPublicId()}, + }, + wantCreds: []credential.Static{ + updCred1, updCred2, + }, + }, { name: "valid-ssh-pk-cred", args: args{ @@ -147,10 +169,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()}, + credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCredWithPass.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId(), jsonCred1.GetPublicId(), jsonCred2.GetPublicId(), updCred1.GetPublicId(), updCred2.GetPublicId()}, }, wantCreds: []credential.Static{ - upCred1, spkCred1, spkCredWithPass, spkCred2, upCred2, jsonCred1, jsonCred2, + upCred1, spkCred1, spkCredWithPass, spkCred2, upCred2, jsonCred1, jsonCred2, updCred1, updCred2, }, }, } @@ -170,6 +192,7 @@ func TestRepository_Retrieve(t *testing.T) { gotCreds, cmpopts.IgnoreUnexported( UsernamePasswordCredential{}, store.UsernamePasswordCredential{}, + UsernamePasswordDomainCredential{}, store.UsernamePasswordDomainCredential{}, SshPrivateKeyCredential{}, store.SshPrivateKeyCredential{}, JsonCredential{}, store.JsonCredential{}), cmpopts.IgnoreTypes(×tamp.Timestamp{}), diff --git a/internal/credential/static/rewrapping_test.go b/internal/credential/static/rewrapping_test.go index e0610b779..0fa4d4393 100644 --- a/internal/credential/static/rewrapping_test.go +++ b/internal/credential/static/rewrapping_test.go @@ -284,7 +284,8 @@ func TestRewrap_credStaticUsernamePasswordDomainRewrapFn(t *testing.T) { _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) - cred, err := NewUsernamePasswordDomainCredential(cs.GetPublicId(), "username", "domain", "password") + + cred, err := NewUsernamePasswordDomainCredential(cs.GetPublicId(), "username", "password", "domain") assert.NoError(t, err) cred.PublicId, err = credential.NewUsernamePasswordCredentialId(ctx) diff --git a/internal/credential/static/testing.go b/internal/credential/static/testing.go index fd42d3591..506fce34b 100644 --- a/internal/credential/static/testing.go +++ b/internal/credential/static/testing.go @@ -213,6 +213,77 @@ func TestUsernamePasswordCredentials( return creds } +// TestUsernamePasswordDomainCredential creates a username password domain 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 TestUsernamePasswordDomainCredential( + t testing.TB, + conn *db.DB, + wrapper wrapping.Wrapper, + username, password, domain, storeId, projectId string, + opts ...Option, +) *UsernamePasswordDomainCredential { + 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 := NewUsernamePasswordDomainCredential(storeId, username, credential.Password(password), domain, opts...) + require.NoError(t, err) + require.NotNil(t, cred) + + id := opt.withPublicId + if id == "" { + id, err = credential.NewUsernamePasswordDomainCredentialId(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 +} + +// TestUsernamePasswordDomainCredentials creates count number of username password domain 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 TestUsernamePasswordDomainCredentials( + t testing.TB, + conn *db.DB, + wrapper wrapping.Wrapper, + username, password, domain, storeId, projectId string, + count int, +) []*UsernamePasswordDomainCredential { + 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([]*UsernamePasswordDomainCredential, 0, count) + for i := 0; i < count; i++ { + creds = append(creds, TestUsernamePasswordDomainCredential(t, conn, wrapper, username, password, domain, 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/credential/static/usernamepassworddomain_credential.go b/internal/credential/static/usernamepassworddomain_credential.go index a16827530..3d0eb1010 100644 --- a/internal/credential/static/usernamepassworddomain_credential.go +++ b/internal/credential/static/usernamepassworddomain_credential.go @@ -33,8 +33,9 @@ type UsernamePasswordDomainCredential struct { func NewUsernamePasswordDomainCredential( storeId string, username string, - domain string, password credential.Password, + domain string, + opt ...Option, ) (*UsernamePasswordDomainCredential, error) { opts := getOpts(opt...) diff --git a/internal/credential/static/usernamepassworddomain_credential_test.go b/internal/credential/static/usernamepassworddomain_credential_test.go index 23c38980e..67202e246 100644 --- a/internal/credential/static/usernamepassworddomain_credential_test.go +++ b/internal/credential/static/usernamepassworddomain_credential_test.go @@ -173,7 +173,8 @@ func TestUsernamePasswordDomainCredential_New(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) ctx := context.Background() - got, err := NewUsernamePasswordDomainCredential(tt.args.storeId, tt.args.username, tt.args.domain, tt.args.password, tt.args.options...) + + got, err := NewUsernamePasswordDomainCredential(tt.args.storeId, tt.args.username, tt.args.password, tt.args.domain, tt.args.options...) require.NoError(err) require.NotNil(got) assert.Emptyf(got.PublicId, "PublicId set")