feat(credential/static): Implement CRUDL support for password credentials (#6163)

llb-password-credential-type-clean
Bharath Gajjala 7 months ago
parent 9f2bcfa4b7
commit 205cb64d8b

@ -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"))

File diff suppressed because it is too large Load Diff

@ -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(&timestamp.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"`

Loading…
Cancel
Save