feat(credential/static): Implement CRUDL support for password credentials (#6163)
This commit is contained in:
1 parent
82745e16be
commit
ec7450961c
14 files changed
+1775
-49
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,6 +2465,432 @@ func TestRepository_UpdateUsernamePasswordDomainCredential(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 *PasswordCredential) *PasswordCredential {
|
||||
return func(c *PasswordCredential) *PasswordCredential {
|
||||
c.Name = n
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
changeDescription := func(d string) func(*PasswordCredential) *PasswordCredential {
|
||||
return func(c *PasswordCredential) *PasswordCredential {
|
||||
c.Description = d
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
makeNil := func() func(*PasswordCredential) *PasswordCredential {
|
||||
return func(_ *PasswordCredential) *PasswordCredential {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
makeEmbeddedNil := func() func(*PasswordCredential) *PasswordCredential {
|
||||
return func(_ *PasswordCredential) *PasswordCredential {
|
||||
return &PasswordCredential{}
|
||||
}
|
||||
}
|
||||
|
||||
setPublicId := func(n string) func(*PasswordCredential) *PasswordCredential {
|
||||
return func(c *PasswordCredential) *PasswordCredential {
|
||||
c.PublicId = n
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
deleteStoreId := func() func(*PasswordCredential) *PasswordCredential {
|
||||
return func(c *PasswordCredential) *PasswordCredential {
|
||||
c.StoreId = ""
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
deleteVersion := func() func(*PasswordCredential) *PasswordCredential {
|
||||
return func(c *PasswordCredential) *PasswordCredential {
|
||||
c.Version = 0
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
changePassword := func(d string) func(*PasswordCredential) *PasswordCredential {
|
||||
return func(c *PasswordCredential) *PasswordCredential {
|
||||
c.Password = []byte(d)
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
combine := func(fns ...func(cs *PasswordCredential) *PasswordCredential) func(*PasswordCredential) *PasswordCredential {
|
||||
return func(c *PasswordCredential) *PasswordCredential {
|
||||
for _, fn := range fns {
|
||||
c = fn(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
orig *PasswordCredential
|
||||
chgFn func(*PasswordCredential) *PasswordCredential
|
||||
masks []string
|
||||
want *PasswordCredential
|
||||
wantCount int
|
||||
wantErr errors.Code
|
||||
}{
|
||||
{
|
||||
name: "nil-credential",
|
||||
orig: &PasswordCredential{
|
||||
PasswordCredential: &store.PasswordCredential{
|
||||
Password: []byte("pass"),
|
||||
},
|
||||
},
|
||||
chgFn: makeNil(),
|
||||
masks: []string{"Name", "Description"},
|
||||
wantErr: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "nil-embedded-credential",
|
||||
orig: &PasswordCredential{
|
||||
PasswordCredential: &store.PasswordCredential{
|
||||
Password: []byte("pass"),
|
||||
},
|
||||
},
|
||||
chgFn: makeEmbeddedNil(),
|
||||
masks: []string{"Name", "Description"},
|
||||
wantErr: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "no-public-id",
|
||||
orig: &PasswordCredential{
|
||||
PasswordCredential: &store.PasswordCredential{
|
||||
Password: []byte("pass"),
|
||||
},
|
||||
},
|
||||
chgFn: setPublicId(""),
|
||||
masks: []string{"Name", "Description"},
|
||||
wantErr: errors.InvalidPublicId,
|
||||
},
|
||||
{
|
||||
name: "no-store-id",
|
||||
orig: &PasswordCredential{
|
||||
PasswordCredential: &store.PasswordCredential{
|
||||
Password: []byte("pass"),
|
||||
},
|
||||
},
|
||||
chgFn: deleteStoreId(),
|
||||
masks: []string{"Name", "Description"},
|
||||
wantErr: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "no-version",
|
||||
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")
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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{}),
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in new issue
Block a user