Add ssh private key type and add type to static store (#2262)

Co-authored-by: Louis Ruch <louisruch@gmail.com>
This commit is contained in:
Jeff MitchellandLouis Ruch authored and GitHub committed 2022-07-18 19:16:11 -04:00
1 parent 4b3882c122
commit 011e2e7425
25 files changed
+2633 -320

No files matched your search

+1 -1
View File
@@ -72,7 +72,7 @@ require (
github.com/stretchr/testify v1.7.2
github.com/zalando/go-keyring v0.2.1
go.uber.org/atomic v1.9.0
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
+2 -2
View File
@@ -1361,8 +1361,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+1
View File
@@ -28,6 +28,7 @@ type Type string
const (
UnspecifiedType Type = "unspecified"
UsernamePasswordType Type = "username_password"
SshPrivateKeyType Type = "ssh_private_key"
)
// A Library is a resource that provides credentials that are of the same
+43
View File
@@ -0,0 +1,43 @@
package credential
import (
"context"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/types/subtypes"
)
func init() {
if err := subtypes.Register(Domain, UsernamePasswordSubtype, UsernamePasswordCredentialPrefix, PreviousUsernamePasswordCredentialPrefix); err != nil {
panic(err)
}
if err := subtypes.Register(Domain, SshPrivateKeySubtype, SshPrivateKeyCredentialPrefix); err != nil {
panic(err)
}
}
const (
UsernamePasswordCredentialPrefix = "credup"
PreviousUsernamePasswordCredentialPrefix = "cred"
UsernamePasswordSubtype = subtypes.Subtype("username_password")
SshPrivateKeyCredentialPrefix = "credspk"
SshPrivateKeySubtype = subtypes.Subtype("ssh_private_key")
)
func NewUsernamePasswordCredentialId(ctx context.Context) (string, error) {
id, err := db.NewPublicId(UsernamePasswordCredentialPrefix)
if err != nil {
return "", errors.Wrap(ctx, err, "credential.NewUsernamePasswordCredentialId")
}
return id, nil
}
func NewSshPrivateKeyCredentialId(ctx context.Context) (string, error) {
id, err := db.NewPublicId(SshPrivateKeyCredentialPrefix)
if err != nil {
return "", errors.Wrap(ctx, err, "credential.NewSshPrivateKeyCredentialId")
}
return id, nil
}
+1
View File
@@ -6,4 +6,5 @@ const (
descriptionField = "Description"
usernameField = "Username"
passwordField = "Password"
privateKeyField = "PrivateKey"
)
+3 -17
View File
@@ -13,20 +13,14 @@ func init() {
if err := subtypes.Register(credential.Domain, Subtype, CredentialStorePrefix, PreviousCredentialStorePrefix); err != nil {
panic(err)
}
if err := subtypes.Register(credential.Domain, UsernamePasswordSubtype, UsernamePasswordCredentialPrefix, PreviousUsernamePasswordCredentialPrefix); err != nil {
panic(err)
}
}
// PublicId prefixes for the resources in the static package.
const (
CredentialStorePrefix = "csst"
PreviousCredentialStorePrefix = "cs"
UsernamePasswordCredentialPrefix = "credup"
PreviousUsernamePasswordCredentialPrefix = "cred"
CredentialStorePrefix = "csst"
PreviousCredentialStorePrefix = "cs"
Subtype = subtypes.Subtype("static")
UsernamePasswordSubtype = subtypes.Subtype("username_password")
Subtype = subtypes.Subtype("static")
)
func newCredentialStoreId(ctx context.Context) (string, error) {
@@ -36,11 +30,3 @@ func newCredentialStoreId(ctx context.Context) (string, error) {
}
return id, nil
}
func newUsernamePasswordCredentialId(ctx context.Context) (string, error) {
id, err := db.NewPublicId(UsernamePasswordCredentialPrefix)
if err != nil {
return "", errors.Wrap(ctx, err, "static.newUsernamePasswordCredentialId")
}
return id, nil
}
@@ -5,11 +5,13 @@ import (
"fmt"
"strings"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/db"
dbcommon "github.com/hashicorp/boundary/internal/db/common"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/subtypes"
)
// CreateUsernamePasswordCredential inserts c into the repository and returns a new
@@ -53,7 +55,7 @@ func (r *Repository) CreateUsernamePasswordCredential(
}
c = c.clone()
id, err := newUsernamePasswordCredentialId(ctx)
id, err := credential.NewUsernamePasswordCredentialId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
@@ -91,33 +93,139 @@ func (r *Repository) CreateUsernamePasswordCredential(
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in store: %s", c.StoreId)))
}
// Clear password fields, only passwordHmac should be returned
// 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
// assigned by this method. c must contain a valid StoreId.
//
// The private key is encrypted and a HmacSha256 of the private key is
// calculated. Only the PrivateKeyHmac is returned, the plain-text and encrypted
// private key is not returned.
//
// Both c.Name and c.Description are optional. If c.Name is set, it must be
// unique within c.ScopeId. Both c.CreateTime and c.UpdateTime are ignored.
func (r *Repository) CreateSshPrivateKeyCredential(
ctx context.Context,
scopeId string,
c *SshPrivateKeyCredential,
_ ...Option,
) (*SshPrivateKeyCredential, error) {
const op = "static.(Repository).CreateSshPrivateKeyCredential"
if c == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing credential")
}
if c.SshPrivateKeyCredential == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing embedded credential")
}
if scopeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
if c.Username == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing username")
}
if c.PrivateKey == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing private key")
}
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.NewSshPrivateKeyCredentialId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
c.PublicId = id
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, 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, scopeId, 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 *SshPrivateKeyCredential
_, 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 private key fields, only PrivateKeyHmac should be returned
newCred.PrivateKeyEncrypted = nil
newCred.PrivateKey = nil
return newCred, nil
}
// LookupCredential returns the Credential for the publicId. Returns
// nil, nil if no Credential is found for the publicId.
// TODO: This should hit a view and return the interface type...
func (r *Repository) LookupCredential(ctx context.Context, publicId string, _ ...Option) (*UsernamePasswordCredential, error) {
func (r *Repository) LookupCredential(ctx context.Context, publicId string, _ ...Option) (credential.Static, error) {
const op = "static.(Repository).LookupCredential"
if publicId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id")
}
cred := allocUsernamePasswordCredential()
cred.PublicId = publicId
if err := r.reader.LookupByPublicId(ctx, cred); 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
cred.CtPassword = nil
cred.Password = nil
var cred credential.Static
switch subtypes.SubtypeFromId(credential.Domain, publicId) {
case credential.UsernamePasswordSubtype:
upCred := allocUsernamePasswordCredential()
upCred.PublicId = publicId
if err := r.reader.LookupByPublicId(ctx, upCred); 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
upCred.CtPassword = nil
upCred.Password = nil
cred = upCred
case credential.SshPrivateKeySubtype:
spkCred := allocSshPrivateKeyCredential()
spkCred.PublicId = publicId
if err := r.reader.LookupByPublicId(ctx, spkCred); 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 private key fields, only privateKeyHmac should be returned
spkCred.PrivateKeyEncrypted = nil
spkCred.PrivateKey = nil
cred = spkCred
}
return cred, nil
}
@@ -141,10 +249,10 @@ func (r *Repository) UpdateUsernamePasswordCredential(ctx context.Context,
) (*UsernamePasswordCredential, int, error) {
const op = "static.(Repository).UpdateUsernamePasswordCredential"
if c == nil {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing Credential")
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing credential")
}
if c.UsernamePasswordCredential == nil {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded Credential")
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")
@@ -229,17 +337,132 @@ func (r *Repository) UpdateUsernamePasswordCredential(ctx context.Context,
return nil, db.NoRowsAffected, err
}
// Clear password fields, only passwordHmac should be returned
// 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
// number of records updated. c is not changed.
//
// c must contain a valid PublicId. Only Name, Description, Username and
// PrivateKey can be changed. If c.Name is set to a non-empty string, it must be
// unique within c.ScopeId.
//
// 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) UpdateSshPrivateKeyCredential(ctx context.Context,
scopeId string,
c *SshPrivateKeyCredential,
version uint32,
fieldMaskPaths []string,
_ ...Option,
) (*SshPrivateKeyCredential, int, error) {
const op = "static.(Repository).UpdateSshPrivateKeyCredential"
if c == nil {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing credential")
}
if c.SshPrivateKeyCredential == 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 scopeId == "" {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
if c.StoreId == "" {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing store id")
}
c = c.clone()
for _, f := range fieldMaskPaths {
switch {
case strings.EqualFold(nameField, f):
case strings.EqualFold(descriptionField, f):
case strings.EqualFold(usernameField, f):
case strings.EqualFold(privateKeyField, f):
default:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f)
}
}
dbMask, nullFields := dbcommon.BuildUpdatePaths(
map[string]interface{}{
nameField: c.Name,
descriptionField: c.Description,
usernameField: c.Username,
privateKeyField: c.PrivateKey,
},
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(privateKeyField, f) {
// Private key has been updated, re-encrypt and recalculate hmac
databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, 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 PrivateKeyHmac and PrivateKeyEncrypted masks for update.
dbMask = append(dbMask, "PrivateKeyHmac", "PrivateKeyEncrypted")
}
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, 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 *SshPrivateKeyCredential
_, 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 private key fields, only PrivateKeyHmac should be returned
returnedCredential.PrivateKeyEncrypted = nil
returnedCredential.PrivateKey = nil
return returnedCredential, rowsUpdated, nil
}
// ListCredentials returns a slice of UsernamePasswordCredentials for the
// scopeIds. WithLimit is the only option supported.
// TODO: This should hit a view and return the interface type...
func (r *Repository) ListCredentials(ctx context.Context, storeId string, opt ...Option) ([]*UsernamePasswordCredential, error) {
func (r *Repository) ListCredentials(ctx context.Context, storeId string, opt ...Option) ([]credential.Static, error) {
const op = "static.(Repository).ListCredentials"
if storeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no storeId")
@@ -250,19 +473,36 @@ func (r *Repository) ListCredentials(ctx context.Context, storeId string, opt ..
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
}
var creds []*UsernamePasswordCredential
err := r.reader.SearchWhere(ctx, &creds, "store_id = ?", []interface{}{storeId}, db.WithLimit(limit))
var upCreds []*UsernamePasswordCredential
err := r.reader.SearchWhere(ctx, &upCreds, "store_id = ?", []interface{}{storeId}, db.WithLimit(limit))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, c := range creds {
// Clear password fields, only passwordHmac should be returned
c.CtPassword = nil
c.Password = nil
var spkCreds []*SshPrivateKeyCredential
err = r.reader.SearchWhere(ctx, &spkCreds, "store_id = ?", []interface{}{storeId}, db.WithLimit(limit))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return creds, nil
ret := make([]credential.Static, 0, len(upCreds)+len(spkCreds))
for _, c := range upCreds {
// Clear password fields, only PasswordHmac should be returned
c.CtPassword = nil
c.Password = nil
ret = append(ret, c)
}
for _, c := range spkCreds {
// Clear private key fields, only PrivateKeyHmac should be returned
c.PrivateKeyEncrypted = nil
c.PrivateKey = nil
ret = append(ret, c)
}
return ret, nil
}
// DeleteCredential deletes publicId from the repository and returns
@@ -277,8 +517,22 @@ func (r *Repository) DeleteCredential(ctx context.Context, scopeId, id string, _
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no scope id")
}
c := allocUsernamePasswordCredential()
c.PublicId = id
var input interface{}
var md oplog.Metadata
switch subtypes.SubtypeFromId(credential.Domain, id) {
case credential.UsernamePasswordSubtype:
c := allocUsernamePasswordCredential()
c.PublicId = id
input = c
md = c.oplog(oplog.OpType_OP_TYPE_DELETE)
case credential.SshPrivateKeySubtype:
c := allocSshPrivateKeyCredential()
c.PublicId = id
input = c
md = c.oplog(oplog.OpType_OP_TYPE_DELETE)
default:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "unknown type")
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog)
if err != nil {
@@ -291,7 +545,7 @@ func (r *Repository) DeleteCredential(ctx context.Context, scopeId, id string, _
db.StdRetryCnt,
db.ExpBackoff{},
func(_ db.Reader, w db.Writer) (err error) {
rowsDeleted, err = w.Delete(ctx, c, db.WithOplog(oplogWrapper, c.oplog(oplog.OpType_OP_TYPE_DELETE)))
rowsDeleted, err = w.Delete(ctx, input, db.WithOplog(oplogWrapper, md))
if err != nil {
return errors.Wrap(ctx, err, op)
}
@@ -140,12 +140,14 @@ func TestRepository_CreateCredentialStore(t *testing.T) {
assert.Equal(in.Description, got.Description)
in2, err := NewCredentialStore(prj.GetPublicId(), WithName("my-name"), WithDescription("desc"))
require.NoError(err)
got2, err := repo.CreateCredentialStore(ctx, 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 scope should not conflict
in3, err := NewCredentialStore(org.GetPublicId(), WithName("my-name"), WithDescription("desc"))
require.NoError(err)
got3, err := repo.CreateCredentialStore(ctx, in3)
require.NoError(err)
assert.Equal(in.Name, got3.Name)
@@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/static/store"
"github.com/hashicorp/boundary/internal/db"
dbassert "github.com/hashicorp/boundary/internal/db/assert"
@@ -121,7 +122,7 @@ func TestRepository_CreateUsernamePasswordCredential(t *testing.T) {
return
}
require.NoError(err)
assertPublicId(t, UsernamePasswordCredentialPrefix, got.PublicId)
assertPublicId(t, credential.UsernamePasswordCredentialPrefix, got.PublicId)
assert.Equal(tt.cred.Username, got.Username)
assert.Nil(got.Password)
assert.Nil(got.CtPassword)
@@ -172,12 +173,186 @@ func TestRepository_CreateUsernamePasswordCredential(t *testing.T) {
assert.Equal(in.Description, got.Description)
in2, err := NewUsernamePasswordCredential(prjCs.GetPublicId(), "user", "pass", WithName("my-name"), WithDescription("different"))
require.NoError(err)
got2, err := repo.CreateUsernamePasswordCredential(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 scope should not conflict
in3, err := NewUsernamePasswordCredential(orgCs.GetPublicId(), "user", "pass", WithName("my-name"), WithDescription("different"))
require.NoError(err)
got3, err := repo.CreateUsernamePasswordCredential(ctx, org.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")
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
scopeId string
cred *SshPrivateKeyCredential
wantErr bool
wantErrCode errors.Code
}{
{
name: "missing-store",
wantErr: true,
wantErrCode: errors.InvalidParameter,
},
{
name: "missing-embedded-cred",
cred: &SshPrivateKeyCredential{},
wantErr: true,
wantErrCode: errors.InvalidParameter,
},
{
name: "missing-scope-id",
cred: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "my-user",
PrivateKey: []byte(TestSshPrivateKeyPem),
StoreId: cs.PublicId,
},
},
wantErr: true,
wantErrCode: errors.InvalidParameter,
},
{
name: "missing-username",
scopeId: prj.PublicId,
cred: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
PrivateKey: []byte(TestSshPrivateKeyPem),
StoreId: cs.PublicId,
},
},
wantErr: true,
wantErrCode: errors.InvalidParameter,
},
{
name: "missing-private-key",
scopeId: prj.PublicId,
cred: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "my-user",
StoreId: cs.PublicId,
},
},
wantErr: true,
wantErrCode: errors.InvalidParameter,
},
{
name: "missing-store-id",
scopeId: prj.PublicId,
cred: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "my-user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantErr: true,
wantErrCode: errors.InvalidParameter,
},
{
name: "valid",
scopeId: prj.PublicId,
cred: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "my-user",
PrivateKey: []byte(TestSshPrivateKeyPem),
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.CreateSshPrivateKeyCredential(ctx, tt.scopeId, 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, credential.SshPrivateKeyCredentialPrefix, got.PublicId)
assert.Equal(tt.cred.Username, got.Username)
assert.Nil(got.PrivateKey)
assert.Nil(got.PrivateKeyEncrypted)
// Validate password
lookupCred := allocSshPrivateKeyCredential()
lookupCred.PublicId = got.PublicId
require.NoError(rw.LookupById(ctx, lookupCred))
databaseWrapper, err := kkms.GetWrapper(context.Background(), tt.scopeId, kms.KeyPurposeDatabase)
require.NoError(err)
require.NoError(lookupCred.decrypt(ctx, databaseWrapper))
assert.Equal(tt.cred.PrivateKey, lookupCred.PrivateKey)
assert.Empty(got.PrivateKey)
assert.Empty(got.PrivateKeyEncrypted)
assert.NotEmpty(got.PrivateKeyHmac)
// Validate hmac
hm, err := crypto.HmacSha256(ctx, tt.cred.PrivateKey, databaseWrapper, []byte(tt.cred.StoreId), nil)
require.NoError(err)
assert.Equal([]byte(hm), got.PrivateKeyHmac)
// 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))
require.NoError(err)
prjCs := TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
orgCs := TestCredentialStore(t, conn, wrapper, org.GetPublicId())
in, err := NewUsernamePasswordCredential(prjCs.GetPublicId(), "user", "pass", WithName("my-name"), WithDescription("original"))
assert.NoError(err)
got, err := repo.CreateUsernamePasswordCredential(ctx, prj.PublicId, in)
require.NoError(err)
assert.Equal(in.Name, got.Name)
assert.Equal(in.Description, got.Description)
in2, err := NewUsernamePasswordCredential(prjCs.GetPublicId(), "user", "pass", WithName("my-name"), WithDescription("different"))
require.NoError(err)
got2, err := repo.CreateUsernamePasswordCredential(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 scope should not conflict
in3, err := NewUsernamePasswordCredential(orgCs.GetPublicId(), "user", "pass", WithName("my-name"), WithDescription("different"))
require.NoError(err)
got3, err := repo.CreateUsernamePasswordCredential(ctx, org.GetPublicId(), in3)
require.NoError(err)
assert.Equal(in3.Name, got3.Name)
@@ -195,18 +370,24 @@ func TestRepository_LookupCredential(t *testing.T) {
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
store := TestCredentialStore(t, conn, wrapper, prj.PublicId)
cred := TestUsernamePasswordCredential(t, conn, wrapper, "username", "password", store.PublicId, prj.PublicId)
upCred := TestUsernamePasswordCredential(t, conn, wrapper, "username", "password", store.PublicId, prj.PublicId)
spkCred := TestSshPrivateKeyCredential(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.PublicId, prj.PublicId)
tests := []struct {
name string
id string
want *UsernamePasswordCredential
want credential.Static
wantErr errors.Code
}{
{
name: "valid-with-client-cert",
id: cred.GetPublicId(),
want: cred,
name: "up-valid",
id: upCred.GetPublicId(),
want: upCred,
},
{
name: "spk-valid",
id: spkCred.GetPublicId(),
want: spkCred,
},
{
name: "empty-public-id",
@@ -242,10 +423,19 @@ func TestRepository_LookupCredential(t *testing.T) {
return
}
assert.NotNil(got)
assert.Empty(got.Password)
assert.Empty(got.CtPassword)
assert.NotEmpty(got.PasswordHmac)
require.NotNil(got)
switch v := got.(type) {
case *UsernamePasswordCredential:
assert.Empty(v.Password)
assert.Empty(v.CtPassword)
assert.NotEmpty(v.PasswordHmac)
case *SshPrivateKeyCredential:
assert.Empty(v.PrivateKey)
assert.Empty(v.PrivateKeyEncrypted)
assert.NotEmpty(v.PrivateKeyHmac)
default:
require.Fail("unknown type")
}
})
}
}
@@ -257,15 +447,12 @@ func TestRepository_ListCredentials(t *testing.T) {
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(context.Background(), rw, rw, kms)
assert.NoError(err)
require.NotNil(repo)
total := 10
defaultLimit := 5
total := 20
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), total)
TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), total/2)
TestSshPrivateKeyCredentials(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), total/2)
type args struct {
storeId string
@@ -289,7 +476,7 @@ func TestRepository_ListCredentials(t *testing.T) {
args: args{
storeId: store.PublicId,
},
wantCnt: total,
wantCnt: defaultLimit * 2,
},
{
name: "custom-limit",
@@ -297,7 +484,7 @@ func TestRepository_ListCredentials(t *testing.T) {
storeId: store.PublicId,
opt: []Option{WithLimit(3)},
},
wantCnt: 3,
wantCnt: 3 * 2,
},
{
name: "bad-store",
@@ -308,16 +495,33 @@ func TestRepository_ListCredentials(t *testing.T) {
},
}
for _, tt := range tests {
got, err := repo.ListCredentials(context.Background(), tt.args.storeId, tt.args.opt...)
require.NoError(err)
assert.Equal(tt.wantCnt, len(got))
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(context.Background(), rw, rw, kms)
assert.NoError(err)
require.NotNil(repo)
repo.defaultLimit = defaultLimit
// Validate only passwordHmac is returned
for _, c := range got {
assert.Empty(c.Password)
assert.Empty(c.CtPassword)
assert.NotEmpty(c.PasswordHmac)
}
got, err := repo.ListCredentials(context.Background(), tt.args.storeId, tt.args.opt...)
require.NoError(err)
assert.Equal(tt.wantCnt, len(got))
// Validate only hmac values are returned
for _, c := range got {
switch v := c.(type) {
case *UsernamePasswordCredential:
assert.Empty(v.Password)
assert.Empty(v.CtPassword)
assert.NotEmpty(v.PasswordHmac)
case *SshPrivateKeyCredential:
assert.Empty(v.PrivateKey)
assert.Empty(v.PrivateKeyEncrypted)
assert.NotEmpty(v.PrivateKeyHmac)
default:
require.Fail("unknown type")
}
}
})
}
}
@@ -331,7 +535,8 @@ func TestRepository_DeleteCredential(t *testing.T) {
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
store := TestCredentialStore(t, conn, wrapper, prj.PublicId)
cred := TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId())
upCred := TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId())
spkCred := TestSshPrivateKeyCredential(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId())
tests := []struct {
name string
@@ -351,8 +556,13 @@ func TestRepository_DeleteCredential(t *testing.T) {
want: 0,
},
{
name: "With existing id",
in: cred.GetPublicId(),
name: "With existing username-password id",
in: upCred.GetPublicId(),
want: 1,
},
{
name: "With existing ssh private key id",
in: spkCred.GetPublicId(),
want: 1,
},
}
@@ -407,9 +617,9 @@ func TestRepository_UpdateUsernamePasswordCredential(t *testing.T) {
}
}
deletePublicId := func() func(*UsernamePasswordCredential) *UsernamePasswordCredential {
setPublicId := func(n string) func(*UsernamePasswordCredential) *UsernamePasswordCredential {
return func(c *UsernamePasswordCredential) *UsernamePasswordCredential {
c.PublicId = ""
c.PublicId = n
return c
}
}
@@ -428,13 +638,6 @@ func TestRepository_UpdateUsernamePasswordCredential(t *testing.T) {
}
}
nonExistentPublicId := func() func(*UsernamePasswordCredential) *UsernamePasswordCredential {
return func(c *UsernamePasswordCredential) *UsernamePasswordCredential {
c.PublicId = "abcd_OOOOOOOOOO"
return c
}
}
changeUser := func(n string) func(credential *UsernamePasswordCredential) *UsernamePasswordCredential {
return func(c *UsernamePasswordCredential) *UsernamePasswordCredential {
c.Username = n
@@ -499,7 +702,7 @@ func TestRepository_UpdateUsernamePasswordCredential(t *testing.T) {
Password: []byte("pass"),
},
},
chgFn: deletePublicId(),
chgFn: setPublicId(""),
masks: []string{"Name", "Description"},
wantErr: errors.InvalidPublicId,
},
@@ -536,7 +739,7 @@ func TestRepository_UpdateUsernamePasswordCredential(t *testing.T) {
Password: []byte("pass"),
},
},
chgFn: combine(nonExistentPublicId(), changeName("test-update-name-repo")),
chgFn: combine(setPublicId("abcd_OOOOOOOOOO"), changeName("test-update-name-repo")),
masks: []string{"Name"},
wantErr: errors.RecordNotFound,
},
@@ -853,7 +1056,7 @@ func TestRepository_UpdateUsernamePasswordCredential(t *testing.T) {
assert.NoError(err)
assert.Empty(tt.orig.PublicId)
require.NotNil(got)
assertPublicId(t, UsernamePasswordCredentialPrefix, got.PublicId)
assertPublicId(t, credential.UsernamePasswordCredentialPrefix, got.PublicId)
assert.Equal(tt.wantCount, gotCount, "row count")
assert.NotSame(tt.orig, got)
assert.Equal(tt.orig.StoreId, got.StoreId)
@@ -894,3 +1097,526 @@ func TestRepository_UpdateUsernamePasswordCredential(t *testing.T) {
})
}
}
func TestRepository_UpdateSshPrivateKeyCredential(t *testing.T) {
const testSecondarySshPrivateKeyPem = `
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQAAAJDmpbfr5qW3
6wAAAAtzc2gtZWQyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQ
AAAEBvvkQkH06ad2GpX1VVARzu9NkHA6gzamAaQ/hkn5FuZvF/CEQBkquextDE6NUDc8WI
Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ=
-----END OPENSSH PRIVATE KEY-----
`
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
changeName := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
c.Name = n
return c
}
}
changeDescription := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
c.Description = d
return c
}
}
makeNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential {
return nil
}
}
makeEmbeddedNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential {
return &SshPrivateKeyCredential{}
}
}
setPublicId := func(n string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
c.PublicId = n
return c
}
}
deleteStoreId := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
c.StoreId = ""
return c
}
}
deleteVersion := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
c.Version = 0
return c
}
}
changeUser := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
c.Username = n
return c
}
}
changePrivateKey := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
c.PrivateKey = []byte(d)
return c
}
}
combine := func(fns ...func(cs *SshPrivateKeyCredential) *SshPrivateKeyCredential) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
for _, fn := range fns {
c = fn(c)
}
return c
}
}
tests := []struct {
name string
orig *SshPrivateKeyCredential
chgFn func(*SshPrivateKeyCredential) *SshPrivateKeyCredential
masks []string
want *SshPrivateKeyCredential
wantCount int
wantErr errors.Code
}{
{
name: "nil-credential",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: makeNil(),
masks: []string{"Name", "Description"},
wantErr: errors.InvalidParameter,
},
{
name: "nil-embedded-credential",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: makeEmbeddedNil(),
masks: []string{"Name", "Description"},
wantErr: errors.InvalidParameter,
},
{
name: "no-public-id",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: setPublicId(""),
masks: []string{"Name", "Description"},
wantErr: errors.InvalidPublicId,
},
{
name: "no-store-id",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: deleteStoreId(),
masks: []string{"Name", "Description"},
wantErr: errors.InvalidParameter,
},
{
name: "no-version",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: deleteVersion(),
masks: []string{"Name", "Description"},
wantErr: errors.InvalidParameter,
},
{
name: "updating-non-existent-credential",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: combine(setPublicId("abcd_OOOOOOOOOO"), changeName("test-update-name-repo")),
masks: []string{"Name"},
wantErr: errors.RecordNotFound,
},
{
name: "empty-field-mask",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: changeName("test-update-name-repo"),
wantErr: errors.EmptyFieldMask,
},
{
name: "read-only-fields-in-field-mask",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: changeName("test-update-name-repo"),
masks: []string{"PublicId", "CreateTime", "UpdateTime", "ScopeId"},
wantErr: errors.InvalidFieldMask,
},
{
name: "unknown-field-in-field-mask",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: changeName("test-update-name-repo"),
masks: []string{"Bilbo"},
wantErr: errors.InvalidFieldMask,
},
{
name: "change-name",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: changeName("test-update-name-repo"),
masks: []string{"Name"},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-update-name-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "change-description",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Description: "test-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: changeDescription("test-update-description-repo"),
masks: []string{"Description"},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Description: "test-update-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "change-name-and-description",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Description: "test-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")),
masks: []string{"Name", "Description"},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-update-name-repo",
Description: "test-update-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "change-username",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: changeUser("test-update-user"),
masks: []string{"Username"},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "test-update-user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "change-private-keyu",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: changePrivateKey(testSecondarySshPrivateKeyPem),
masks: []string{"PrivateKey"},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(testSecondarySshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "change-username-and-private-key",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
chgFn: combine(changeUser("test-update-user"), changePrivateKey(testSecondarySshPrivateKeyPem)),
masks: []string{"Username", "PrivateKey"},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "test-update-user",
PrivateKey: []byte(testSecondarySshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "do-not-delete-private-key",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
masks: []string{"Username"},
chgFn: combine(changeUser("test-new-user"), changePrivateKey("")),
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "test-new-user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "do-not-delete-username",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
masks: []string{"PrivateKey"},
chgFn: combine(changeUser(""), changePrivateKey(testSecondarySshPrivateKeyPem)),
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(testSecondarySshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "delete-name",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Description: "test-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
masks: []string{"Name"},
chgFn: combine(changeDescription("test-update-description-repo"), changeName("")),
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Description: "test-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "delete-description",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Description: "test-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
masks: []string{"Description"},
chgFn: combine(changeDescription(""), changeName("test-update-name-repo")),
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "do-not-delete-name",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Description: "test-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
masks: []string{"Description"},
chgFn: combine(changeDescription("test-update-description-repo"), changeName("")),
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Description: "test-update-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
wantCount: 1,
},
{
name: "do-not-delete-description",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-name-repo",
Description: "test-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
masks: []string{"Name"},
chgFn: combine(changeDescription(""), changeName("test-update-name-repo")),
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Name: "test-update-name-repo",
Description: "test-description-repo",
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
},
},
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.CreateSshPrivateKeyCredential(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.UpdateSshPrivateKeyCredential(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, credential.SshPrivateKeyCredentialPrefix, got.PublicId)
assert.Equal(tt.wantCount, gotCount, "row count")
assert.NotSame(tt.orig, got)
assert.Equal(tt.orig.StoreId, got.StoreId)
underlyingDB, err := conn.SqlDB(ctx)
require.NoError(err)
dbassert := dbassert.New(t, underlyingDB)
if tt.want.Name == "" {
got := got.clone()
dbassert.IsNull(got, "name")
} else {
assert.Equal(tt.want.Name, got.Name)
}
if tt.want.Description == "" {
got := got.clone()
dbassert.IsNull(got, "description")
} else {
assert.Equal(tt.want.Description, got.Description)
}
assert.Equal(tt.want.Username, got.Username)
// Validate only PrivateKeyHmac is returned
assert.Empty(got.PrivateKey)
assert.Empty(got.PrivateKeyEncrypted)
assert.NotEmpty(got.PrivateKeyHmac)
// Validate hmac
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase)
require.NoError(err)
hm, err := crypto.HmacSha256(ctx, tt.want.PrivateKey, databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(err)
assert.Equal([]byte(hm), got.PrivateKeyHmac)
if tt.wantCount > 0 {
assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
}
})
}
}
@@ -0,0 +1,138 @@
package static
import (
"context"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/static/store"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/libs/crypto"
"github.com/hashicorp/boundary/internal/oplog"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
"golang.org/x/crypto/ssh"
"google.golang.org/protobuf/proto"
)
var _ credential.Static = (*SshPrivateKeyCredential)(nil)
// A SshPrivateKeyCredential contains the credential with a username and private key.
// It is owned by a credential store.
type SshPrivateKeyCredential struct {
*store.SshPrivateKeyCredential
tableName string `gorm:"-"`
}
// NewSshPrivateKeyCredential creates a new in memory static Credential containing a
// username and private key that is assigned to storeId. Name and description are the only
// valid options. All other options are ignored.
func NewSshPrivateKeyCredential(
ctx context.Context,
storeId string,
username string,
privateKey credential.PrivateKey,
opt ...Option,
) (*SshPrivateKeyCredential, error) {
const op = "static.NewSshPrivateKeyCredential"
if len(privateKey) != 0 {
_, err := ssh.ParsePrivateKey(privateKey)
switch {
case err == nil:
case err.Error() == (&ssh.PassphraseMissingError{}).Error():
// This is okay, if it's brokered and the client can use it, no worries
default:
return nil, errors.Wrap(ctx, err, op)
}
}
opts := getOpts(opt...)
l := &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
StoreId: storeId,
Name: opts.withName,
Description: opts.withDescription,
Username: username,
PrivateKey: privateKey,
},
}
return l, nil
}
func allocSshPrivateKeyCredential() *SshPrivateKeyCredential {
return &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{},
}
}
func (c *SshPrivateKeyCredential) clone() *SshPrivateKeyCredential {
cp := proto.Clone(c.SshPrivateKeyCredential)
return &SshPrivateKeyCredential{
SshPrivateKeyCredential: cp.(*store.SshPrivateKeyCredential),
}
}
// TableName returns the table name.
func (c *SshPrivateKeyCredential) TableName() string {
if c.tableName != "" {
return c.tableName
}
return "credential_static_ssh_private_key_credential"
}
// SetTableName sets the table name.
func (c *SshPrivateKeyCredential) SetTableName(n string) {
c.tableName = n
}
func (c *SshPrivateKeyCredential) oplog(op oplog.OpType) oplog.Metadata {
metadata := oplog.Metadata{
"resource-public-id": []string{c.PublicId},
"resource-type": []string{"credential-static-ssh-private-key"},
"op-type": []string{op.String()},
}
if c.StoreId != "" {
metadata["store-id"] = []string{c.StoreId}
}
return metadata
}
func (c *SshPrivateKeyCredential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "static.(SshPrivateKeyCredential).encrypt"
if len(c.PrivateKey) == 0 {
return errors.New(ctx, errors.InvalidParameter, op, "no private key defined")
}
if err := structwrapping.WrapStruct(ctx, cipher, c.SshPrivateKeyCredential, 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.hmacPrivateKey(ctx, cipher); err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
}
func (c *SshPrivateKeyCredential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "static.(SshPrivateKeyCredential).decrypt"
if err := structwrapping.UnwrapStruct(ctx, cipher, c.SshPrivateKeyCredential, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
return nil
}
func (c *SshPrivateKeyCredential) hmacPrivateKey(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "static.(SshPrivateKeyCredential).hmacPrivateKey"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
hm, err := crypto.HmacSha256(ctx, c.PrivateKey, cipher, []byte(c.StoreId), nil)
if err != nil {
return errors.Wrap(ctx, err, op)
}
c.PrivateKeyHmac = []byte(hm)
return nil
}
@@ -0,0 +1,196 @@
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 TestSshPrivateKeyCredential_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)
privKey := []byte(TestSshPrivateKeyPem)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
cs := TestCredentialStore(t, conn, wrapper, prj.PublicId)
type args struct {
username string
sshPrivateKey credential.PrivateKey
storeId string
options []Option
}
tests := []struct {
name string
args args
want *SshPrivateKeyCredential
wantCreateErr bool
wantEncryptErr bool
wantAllocError bool
}{
{
name: "missing-private-key",
args: args{
username: "test-user",
storeId: cs.PublicId,
},
want: allocSshPrivateKeyCredential(),
wantEncryptErr: true,
},
{
name: "missing-username",
args: args{
sshPrivateKey: privKey,
storeId: cs.PublicId,
},
want: allocSshPrivateKeyCredential(),
wantCreateErr: true,
},
{
name: "missing-store-id",
args: args{
username: "test-user",
sshPrivateKey: privKey,
},
want: allocSshPrivateKeyCredential(),
wantCreateErr: true,
},
{
name: "bad-private-key",
args: args{
username: "test-user",
sshPrivateKey: []byte("foobar"),
storeId: cs.PublicId,
},
want: allocSshPrivateKeyCredential(),
wantAllocError: true,
},
{
name: "valid-no-options",
args: args{
username: "test-user",
sshPrivateKey: privKey,
storeId: cs.PublicId,
},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "test-user",
PrivateKey: privKey,
StoreId: cs.PublicId,
},
},
},
{
name: "valid-with-name",
args: args{
username: "test-user",
sshPrivateKey: privKey,
storeId: cs.PublicId,
options: []Option{WithName("my-credential")},
},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "test-user",
PrivateKey: privKey,
StoreId: cs.PublicId,
Name: "my-credential",
},
},
},
{
name: "valid-with-description",
args: args{
username: "test-user",
sshPrivateKey: privKey,
storeId: cs.PublicId,
options: []Option{WithDescription("my-credential-description")},
},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "test-user",
PrivateKey: privKey,
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 := NewSshPrivateKeyCredential(ctx, tt.args.storeId, tt.args.username, tt.args.sshPrivateKey, tt.args.options...)
if tt.wantAllocError {
assert.Error(err)
return
}
require.NoError(err)
require.NotNil(got)
assert.Emptyf(got.PublicId, "PublicId set")
id, err := credential.NewSshPrivateKeyCredentialId(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 := allocSshPrivateKeyCredential()
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.PrivateKeyEncrypted = nil
// encrypt also calculates the hmac, validate it is correct
hm, err := crypto.HmacSha256(ctx, got.PrivateKey, databaseWrapper, []byte(got.StoreId), nil)
require.NoError(err)
tt.want.PrivateKeyHmac = []byte(hm)
assert.Empty(cmp.Diff(tt.want, got2.clone(), protocmp.Transform()))
})
}
}
+240 -17
View File
@@ -300,6 +300,171 @@ func (x *UsernamePasswordCredential) GetKeyId() string {
return ""
}
type SshPrivateKeyCredential struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// 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 scope_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"`
// username is the username associated with the credential.
// It must be set.
// @inject_tag: `gorm:"not_null"`
Username string `protobuf:"bytes,8,opt,name=username,proto3" json:"username,omitempty" gorm:"not_null"`
// private_key is the plain-text of the private key associated with the
// credential. We are not storing this plain-text private key in the database.
// @inject_tag: `gorm:"-" wrapping:"pt,private_key"`
PrivateKey []byte `protobuf:"bytes,9,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty" gorm:"-" wrapping:"pt,private_key"`
// private_key_encrypted is the ciphertext of the private key. It is stored in
// the database.
// @inject_tag: `gorm:"column:private_key_encrypted;not_null" wrapping:"ct,private_key"`
PrivateKeyEncrypted []byte `protobuf:"bytes,10,opt,name=private_key_encrypted,json=privateKeyEncrypted,proto3" json:"private_key_encrypted,omitempty" gorm:"column:private_key_encrypted;not_null" wrapping:"ct,private_key"`
// private_key_hmac is a sha256-hmac of the unencrypted private key. It is recalculated
// everytime the private key is updated.
// @inject_tag: `gorm:"not_null"`
PrivateKeyHmac []byte `protobuf:"bytes,11,opt,name=private_key_hmac,json=privateKeyHmac,proto3" json:"private_key_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,12,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"`
}
func (x *SshPrivateKeyCredential) Reset() {
*x = SshPrivateKeyCredential{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SshPrivateKeyCredential) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SshPrivateKeyCredential) ProtoMessage() {}
func (x *SshPrivateKeyCredential) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SshPrivateKeyCredential.ProtoReflect.Descriptor instead.
func (*SshPrivateKeyCredential) Descriptor() ([]byte, []int) {
return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{2}
}
func (x *SshPrivateKeyCredential) GetPublicId() string {
if x != nil {
return x.PublicId
}
return ""
}
func (x *SshPrivateKeyCredential) GetCreateTime() *timestamp.Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *SshPrivateKeyCredential) GetUpdateTime() *timestamp.Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
func (x *SshPrivateKeyCredential) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *SshPrivateKeyCredential) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *SshPrivateKeyCredential) GetStoreId() string {
if x != nil {
return x.StoreId
}
return ""
}
func (x *SshPrivateKeyCredential) GetVersion() uint32 {
if x != nil {
return x.Version
}
return 0
}
func (x *SshPrivateKeyCredential) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *SshPrivateKeyCredential) GetPrivateKey() []byte {
if x != nil {
return x.PrivateKey
}
return nil
}
func (x *SshPrivateKeyCredential) GetPrivateKeyEncrypted() []byte {
if x != nil {
return x.PrivateKeyEncrypted
}
return nil
}
func (x *SshPrivateKeyCredential) GetPrivateKeyHmac() []byte {
if x != nil {
return x.PrivateKeyHmac
}
return nil
}
func (x *SshPrivateKeyCredential) GetKeyId() string {
if x != nil {
return x.KeyId
}
return ""
}
var File_controller_storage_credential_static_store_v1_static_proto protoreflect.FileDescriptor
var file_controller_storage_credential_static_store_v1_static_proto_rawDesc = []byte{
@@ -378,11 +543,54 @@ var file_controller_storage_credential_static_store_v1_static_proto_rawDesc = []
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x52, 0x0c, 0x70, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79,
0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64,
0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68,
0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72,
0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65,
0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x2f, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x22, 0xa1, 0x05, 0x0a, 0x17, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b,
0x65, 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65,
0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a,
0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72,
0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31,
0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61,
0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65,
0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f,
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54,
0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73,
0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e,
0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f,
0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b,
0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73,
0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73,
0x74, 0x6f, 0x72, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x12, 0x3f, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01,
0x28, 0x09, 0x42, 0x23, 0xc2, 0xdd, 0x29, 0x1f, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75,
0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d,
0x65, 0x12, 0x49, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79,
0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x28, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x0a, 0x50, 0x72,
0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62,
0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79,
0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x15,
0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x65, 0x6e, 0x63, 0x72,
0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x13, 0x70, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
0x12, 0x5b, 0x0a, 0x10, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f,
0x68, 0x6d, 0x61, 0x63, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x31, 0xc2, 0xdd, 0x29, 0x2d,
0x0a, 0x0e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x6d, 0x61, 0x63,
0x12, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x52, 0x0e, 0x70,
0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x15, 0x0a,
0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b,
0x65, 0x79, 0x49, 0x64, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75,
0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63,
0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63,
0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -397,22 +605,25 @@ 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, 2)
var file_controller_storage_credential_static_store_v1_static_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_controller_storage_credential_static_store_v1_static_proto_goTypes = []interface{}{
(*CredentialStore)(nil), // 0: controller.storage.credential.static.store.v1.CredentialStore
(*UsernamePasswordCredential)(nil), // 1: controller.storage.credential.static.store.v1.UsernamePasswordCredential
(*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp
(*SshPrivateKeyCredential)(nil), // 2: controller.storage.credential.static.store.v1.SshPrivateKeyCredential
(*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp
}
var file_controller_storage_credential_static_store_v1_static_proto_depIdxs = []int32{
2, // 0: controller.storage.credential.static.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
2, // 1: controller.storage.credential.static.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
2, // 2: controller.storage.credential.static.store.v1.UsernamePasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
2, // 3: controller.storage.credential.static.store.v1.UsernamePasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
3, // 0: controller.storage.credential.static.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
3, // 1: controller.storage.credential.static.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
3, // 2: controller.storage.credential.static.store.v1.UsernamePasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
3, // 3: controller.storage.credential.static.store.v1.UsernamePasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
3, // 4: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
3, // 5: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_controller_storage_credential_static_store_v1_static_proto_init() }
@@ -445,6 +656,18 @@ func file_controller_storage_credential_static_store_v1_static_proto_init() {
return nil
}
}
file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SshPrivateKeyCredential); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -452,7 +675,7 @@ func file_controller_storage_credential_static_store_v1_static_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_storage_credential_static_store_v1_static_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
+82 -1
View File
@@ -12,6 +12,16 @@ import (
"github.com/stretchr/testify/require"
)
const TestSshPrivateKeyPem = `
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQAAAJDmpbfr5qW3
6wAAAAtzc2gtZWQyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQ
AAAEBvvkQkH06ad2GpX1VVARzu9NkHA6gzamAaQ/hkn5FuZvF/CEQBkquextDE6NUDc8WI
Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ=
-----END OPENSSH PRIVATE KEY-----
`
// TestCredentialStore creates a static credential store in the provided DB with
// the provided scope and any values passed in through the Options vars.
// If any errors are encountered during the creation of the store, the test will fail.
@@ -96,7 +106,7 @@ func TestUsernamePasswordCredential(
id := opts.withPublicId
if id == "" {
id, err = newUsernamePasswordCredentialId(ctx)
id, err = credential.NewUsernamePasswordCredentialId(ctx)
require.NoError(t, err)
}
cred.PublicId = id
@@ -139,3 +149,74 @@ func TestUsernamePasswordCredentials(
}
return creds
}
// TestSshPrivateKeyCredential creates an ssh private key credential in the
// provided DB with the provided scope and any values passed in through. If any
// errors are encountered during the creation of the store, the test will fail.
func TestSshPrivateKeyCredential(
t testing.TB,
conn *db.DB,
wrapper wrapping.Wrapper,
username, privateKey, storeId, scopeId string,
opt ...Option,
) *SshPrivateKeyCredential {
t.Helper()
ctx := context.Background()
kmsCache := kms.TestKms(t, conn, wrapper)
w := db.New(conn)
opts := getOpts(opt...)
databaseWrapper, err := kmsCache.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
assert.NoError(t, err)
require.NotNil(t, databaseWrapper)
cred, err := NewSshPrivateKeyCredential(ctx, storeId, username, credential.PrivateKey(privateKey), opt...)
require.NoError(t, err)
require.NotNil(t, cred)
id := opts.withPublicId
if id == "" {
id, err = credential.NewSshPrivateKeyCredentialId(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
}
// TestSshPrivateKeyCredentials creates count number of ssh private key
// credentials in the provided DB with the provided scope id. If any errors are
// encountered during the creation of the credentials, the test will fail.
func TestSshPrivateKeyCredentials(
t testing.TB,
conn *db.DB,
wrapper wrapping.Wrapper,
username, privateKey, storeId, scopeId string,
count int,
) []*SshPrivateKeyCredential {
t.Helper()
ctx := context.Background()
kmsCache := kms.TestKms(t, conn, wrapper)
databaseWrapper, err := kmsCache.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
assert.NoError(t, err)
require.NotNil(t, databaseWrapper)
creds := make([]*SshPrivateKeyCredential, 0, count)
for i := 0; i < count; i++ {
creds = append(creds, TestSshPrivateKeyCredential(t, conn, wrapper, username, privateKey, storeId, scopeId))
}
return creds
}
@@ -64,6 +64,7 @@ func Test_TestUsernamePasswordCredential(t *testing.T) {
// Validate hmac
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase)
require.NoError(err)
hm, err := crypto.HmacSha256(context.Background(), cred.Password, databaseWrapper, []byte(cred.StoreId), nil, crypto.WithEd25519())
require.NoError(err)
assert.Equal([]byte(hm), cred.PasswordHmac)
@@ -16,7 +16,7 @@ import (
"google.golang.org/protobuf/testing/protocmp"
)
func TestCredential_New(t *testing.T) {
func TestUsernamePasswordCredential_New(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
@@ -128,13 +128,14 @@ func TestCredential_New(t *testing.T) {
require.NotNil(got)
assert.Emptyf(got.PublicId, "PublicId set")
id, err := newUsernamePasswordCredentialId(ctx)
id, err := credential.NewUsernamePasswordCredentialId(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 {
@@ -26,13 +26,15 @@ import (
)
const (
usernameField = "attributes.username"
passwordField = "attributes.password"
domain = "credential"
usernameField = "attributes.username"
passwordField = "attributes.password"
privateKeyField = "attributes.private_key"
domain = "credential"
)
var (
maskManager handlers.MaskManager
upMaskManager handlers.MaskManager
spkMaskManager handlers.MaskManager
// IdActions contains the set of actions that can be performed on
// individual resources
@@ -53,10 +55,14 @@ var (
func init() {
var err error
if maskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&store.UsernamePasswordCredential{}},
if upMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&store.UsernamePasswordCredential{}},
handlers.MaskSource{&pb.Credential{}, &pb.UsernamePasswordAttributes{}}); err != nil {
panic(err)
}
if spkMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&store.SshPrivateKeyCredential{}},
handlers.MaskSource{&pb.Credential{}, &pb.SshPrivateKeyAttributes{}}); err != nil {
panic(err)
}
}
// Service handles request as described by the pbs.CredentialServiceServer interface.
@@ -293,9 +299,7 @@ func (s Service) listFromRepo(ctx context.Context, storeId string) ([]credential
}
creds := make([]credential.Static, 0, len(up))
for _, s := range up {
creds = append(creds, s)
}
creds = append(creds, up...)
return creds, nil
}
@@ -319,7 +323,7 @@ func (s Service) getFromRepo(ctx context.Context, id string) (credential.Static,
func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Credential) (credential.Static, error) {
const op = "credentials.(Service).createInRepo"
switch item.GetType() {
case static.UsernamePasswordSubtype.String():
case credential.UsernamePasswordSubtype.String():
cred, err := toUsernamePasswordStorageCredential(item.GetCredentialStoreId(), item)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
@@ -336,6 +340,23 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Cred
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential but no error returned from repository.")
}
return out, nil
case credential.SshPrivateKeySubtype.String():
cred, err := toSshPrivateKeyStorageCredential(ctx, item.GetCredentialStoreId(), item)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
repo, err := s.repoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
out, err := repo.CreateSshPrivateKeyCredential(ctx, scopeId, cred)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create credential"))
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential but no error returned from repository.")
}
return out, nil
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, fmt.Sprintf("Unsupported credential type %q", item.GetType()))
}
@@ -352,13 +373,13 @@ func (s Service) updateInRepo(
var dbMasks []string
item := proto.Clone(in).(*pb.Credential)
dbMasks = append(dbMasks, maskManager.Translate(masks)...)
if len(dbMasks) == 0 {
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."})
}
switch subtypes.SubtypeFromId(domain, id) {
case static.UsernamePasswordSubtype:
case credential.UsernamePasswordSubtype:
dbMasks = append(dbMasks, upMaskManager.Translate(masks)...)
if len(dbMasks) == 0 {
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."})
}
cred, err := toUsernamePasswordStorageCredential(storeId, in)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert to username/password storage credential"))
@@ -376,6 +397,31 @@ func (s Service) updateInRepo(
return nil, handlers.NotFoundErrorf("Credential %q doesn't exist or incorrect version provided.", id)
}
return out, nil
case credential.SshPrivateKeySubtype:
dbMasks = append(dbMasks, spkMaskManager.Translate(masks)...)
if len(dbMasks) == 0 {
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."})
}
cred, err := toSshPrivateKeyStorageCredential(ctx, storeId, in)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert to ssh private key storage credential"))
}
cred.PublicId = id
repo, err := s.repoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
out, rowsUpdated, err := repo.UpdateSshPrivateKeyCredential(ctx, scopeId, cred, item.GetVersion(), dbMasks)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential"))
}
if rowsUpdated == 0 {
return nil, handlers.NotFoundErrorf("Credential %q doesn't exist or incorrect version provided.", id)
}
return out, nil
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, fmt.Sprintf("Unsupported credential type %q", item.GetType()))
@@ -465,7 +511,9 @@ func toProto(in credential.Static, opt ...handlers.Option) (*pb.Credential, erro
if outputFields.Has(globals.TypeField) {
switch in.(type) {
case *static.UsernamePasswordCredential:
out.Type = static.UsernamePasswordSubtype.String()
out.Type = credential.UsernamePasswordSubtype.String()
case *static.SshPrivateKeyCredential:
out.Type = credential.SshPrivateKeySubtype.String()
}
}
if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" {
@@ -500,12 +548,21 @@ func toProto(in credential.Static, opt ...handlers.Option) (*pb.Credential, erro
},
}
}
case *static.SshPrivateKeyCredential:
if outputFields.Has(globals.AttributesField) {
out.Attrs = &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String(cred.GetUsername()),
PrivateKeyHmac: base64.RawURLEncoding.EncodeToString(cred.GetPrivateKeyHmac()),
},
}
}
}
return &out, nil
}
func toUsernamePasswordStorageCredential(storeId string, in *pb.Credential) (out *static.UsernamePasswordCredential, err error) {
const op = "credentials.toStorageCredential"
const op = "credentials.toUsernamePasswordStorageCredential"
var opts []static.Option
if in.GetName() != nil {
opts = append(opts, static.WithName(in.GetName().GetValue()))
@@ -527,30 +584,69 @@ func toUsernamePasswordStorageCredential(storeId string, in *pb.Credential) (out
return cs, err
}
func toSshPrivateKeyStorageCredential(ctx context.Context, storeId string, in *pb.Credential) (out *static.SshPrivateKeyCredential, err error) {
const op = "credentials.toSshPrivateKeyStorageCredential"
var opts []static.Option
if in.GetName() != nil {
opts = append(opts, static.WithName(in.GetName().GetValue()))
}
if in.GetDescription() != nil {
opts = append(opts, static.WithDescription(in.GetDescription().GetValue()))
}
attrs := in.GetSshPrivateKeyAttributes()
cs, err := static.NewSshPrivateKeyCredential(
ctx,
storeId,
attrs.GetUsername().GetValue(),
credential.PrivateKey(attrs.GetPrivateKey().GetValue()),
opts...)
if err != nil {
return nil, errors.WrapDeprecated(err, op, errors.WithMsg("unable to build credential"))
}
return cs, err
}
// A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce
// requirements on the structure of the request. They verify that:
// * The path passed in is correctly formatted
// * All required parameters are set
// * There are no conflicting parameters provided
func validateGetRequest(req *pbs.GetCredentialRequest) error {
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, static.UsernamePasswordCredentialPrefix, static.PreviousUsernamePasswordCredentialPrefix)
return handlers.ValidateGetRequest(
handlers.NoopValidatorFn,
req,
credential.UsernamePasswordCredentialPrefix,
credential.PreviousUsernamePasswordCredentialPrefix,
credential.SshPrivateKeyCredentialPrefix,
)
}
func validateCreateRequest(req *pbs.CreateCredentialRequest) error {
return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string {
badFields := map[string]string{}
if req.Item.GetType() != static.UsernamePasswordSubtype.String() {
badFields[globals.TypeField] = fmt.Sprintf("Unsupported credential type %q", req.Item.GetType())
}
if !handlers.ValidId(handlers.Id(req.Item.GetCredentialStoreId()), static.CredentialStorePrefix, static.PreviousCredentialStorePrefix) {
badFields[globals.CredentialStoreIdField] = "This field must be a valid credential store id."
}
if req.Item.GetUsernamePasswordAttributes().GetUsername().GetValue() == "" {
badFields[usernameField] = "Field required for creating a username-password credential."
}
if req.Item.GetUsernamePasswordAttributes().GetPassword().GetValue() == "" {
badFields[passwordField] = "Field required for creating a username-password credential."
switch req.Item.GetType() {
case credential.UsernamePasswordSubtype.String():
if req.Item.GetUsernamePasswordAttributes().GetUsername().GetValue() == "" {
badFields[usernameField] = "Field required for creating a username-password credential."
}
if req.Item.GetUsernamePasswordAttributes().GetPassword().GetValue() == "" {
badFields[passwordField] = "Field required for creating a username-password credential."
}
case credential.SshPrivateKeySubtype.String():
if req.Item.GetSshPrivateKeyAttributes().GetUsername().GetValue() == "" {
badFields[usernameField] = "Field required for creating an SSH private key credential."
}
if req.Item.GetSshPrivateKeyAttributes().GetPrivateKey().GetValue() == "" {
badFields[privateKeyField] = "Field required for creating an SSH private key credential."
}
default:
badFields[globals.TypeField] = fmt.Sprintf("Unsupported credential type %q", req.Item.GetType())
}
return badFields
@@ -560,24 +656,44 @@ func validateCreateRequest(req *pbs.CreateCredentialRequest) error {
func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error {
return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string {
badFields := map[string]string{}
if req.GetItem().GetType() != "" && req.GetItem().GetType() != static.UsernamePasswordSubtype.String() {
switch req.GetItem().GetType() {
case "":
case credential.UsernamePasswordSubtype.String():
attrs := req.GetItem().GetUsernamePasswordAttributes()
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), usernameField) && attrs.GetUsername().GetValue() == "" {
badFields[usernameField] = "This is a required field and cannot be set to empty."
}
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), passwordField) && attrs.GetPassword().GetValue() == "" {
badFields[passwordField] = "This is a required field and cannot be set to empty."
}
case credential.SshPrivateKeySubtype.String():
attrs := req.GetItem().GetSshPrivateKeyAttributes()
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), usernameField) && attrs.GetUsername().GetValue() == "" {
badFields[usernameField] = "This is a required field and cannot be set to empty."
}
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), privateKeyField) && attrs.GetPrivateKey().GetValue() == "" {
badFields[privateKeyField] = "This is a required field and cannot be set to empty."
}
default:
badFields[globals.TypeField] = "Cannot modify resource type."
}
attrs := req.GetItem().GetUsernamePasswordAttributes()
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), usernameField) && attrs.GetUsername().GetValue() == "" {
badFields[usernameField] = "This is a required field and cannot be set to empty."
}
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), passwordField) && attrs.GetPassword().GetValue() == "" {
badFields[passwordField] = "This is a required field and cannot be set to empty."
}
return badFields
}, static.UsernamePasswordCredentialPrefix, static.PreviousUsernamePasswordCredentialPrefix)
},
credential.UsernamePasswordCredentialPrefix,
credential.PreviousUsernamePasswordCredentialPrefix,
credential.SshPrivateKeyCredentialPrefix,
)
}
func validateDeleteRequest(req *pbs.DeleteCredentialRequest) error {
return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, static.UsernamePasswordCredentialPrefix, static.PreviousUsernamePasswordCredentialPrefix)
return handlers.ValidateDeleteRequest(
handlers.NoopValidatorFn,
req,
credential.UsernamePasswordCredentialPrefix,
credential.PreviousUsernamePasswordCredentialPrefix,
credential.SshPrivateKeyCredentialPrefix,
)
}
func validateListRequest(req *pbs.ListCredentialsRequest) error {
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/credential/vault"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
@@ -52,17 +53,16 @@ func TestList(t *testing.T) {
store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
storeNoCreds := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase)
require.NoError(t, err)
var wantCreds []*pb.Credential
for i := 0; i < 10; i++ {
user := fmt.Sprintf("user-%d", i)
pass := fmt.Sprintf("pass-%d", i)
c := static.TestUsernamePasswordCredential(t, conn, wrapper, user, pass, store.GetPublicId(), prj.GetPublicId())
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase)
require.NoError(t, err)
hm, err := crypto.HmacSha256(context.Background(), []byte(pass), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519())
require.NoError(t, err)
wantCreds = append(wantCreds, &pb.Credential{
Id: c.GetPublicId(),
CredentialStoreId: store.GetPublicId(),
@@ -70,7 +70,7 @@ func TestList(t *testing.T) {
CreatedTime: c.GetCreateTime().GetTimestamp(),
UpdatedTime: c.GetUpdateTime().GetTimestamp(),
Version: c.GetVersion(),
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
AuthorizedActions: testAuthorizedActions,
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
@@ -79,6 +79,26 @@ func TestList(t *testing.T) {
},
},
})
spk := static.TestSshPrivateKeyCredential(t, conn, wrapper, user, static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId())
hm, err = crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
wantCreds = append(wantCreds, &pb.Credential{
Id: spk.GetPublicId(),
CredentialStoreId: store.GetPublicId(),
Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()},
CreatedTime: spk.GetCreateTime().GetTimestamp(),
UpdatedTime: spk.GetUpdateTime().GetTimestamp(),
Version: spk.GetVersion(),
Type: credential.SshPrivateKeySubtype.String(),
AuthorizedActions: testAuthorizedActions,
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String(c.GetUsername()),
PrivateKeyHmac: base64.RawURLEncoding.EncodeToString([]byte(hm)),
},
},
})
}
cases := []struct {
@@ -109,7 +129,7 @@ func TestList(t *testing.T) {
{
name: "Filter on Attribute",
req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/username"==%q`, wantCreds[2].GetUsernamePasswordAttributes().GetUsername().Value)},
res: &pbs.ListCredentialsResponse{Items: wantCreds[2:3]},
res: &pbs.ListCredentialsResponse{Items: wantCreds[2:4]},
anonRes: &pbs.ListCredentialsResponse{}, // anonymous user does not have access to attributes
},
{
@@ -169,17 +189,20 @@ func TestGet(t *testing.T) {
}
_, prj := iam.TestScopes(t, iamRepo)
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase)
require.NoError(t, err)
store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
cred := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId())
credPrev := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), static.WithPublicId(fmt.Sprintf("%s_1234567890", static.PreviousUsernamePasswordCredentialPrefix)))
s, err := NewService(staticRepoFn, iamRepoFn)
require.NoError(t, err)
// Calculate hmac
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase)
upCred := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId())
upCredPrev := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), static.WithPublicId(fmt.Sprintf("%s_1234567890", credential.PreviousUsernamePasswordCredentialPrefix)))
upHm, err := crypto.HmacSha256(context.Background(), []byte("pass"), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519())
require.NoError(t, err)
hm, err := crypto.HmacSha256(context.Background(), []byte("pass"), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519())
spkCred := static.TestSshPrivateKeyCredential(t, conn, wrapper, "user", static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId())
spkHm, err := crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
cases := []struct {
@@ -189,44 +212,66 @@ func TestGet(t *testing.T) {
err error
}{
{
name: "success",
id: cred.GetPublicId(),
name: "success-up",
id: upCred.GetPublicId(),
res: &pbs.GetCredentialResponse{
Item: &pb.Credential{
Id: cred.GetPublicId(),
CredentialStoreId: cred.GetStoreId(),
Id: upCred.GetPublicId(),
CredentialStoreId: upCred.GetStoreId(),
Scope: &scopepb.ScopeInfo{Id: store.GetScopeId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()},
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
AuthorizedActions: testAuthorizedActions,
CreatedTime: cred.CreateTime.GetTimestamp(),
UpdatedTime: cred.UpdateTime.GetTimestamp(),
CreatedTime: upCred.CreateTime.GetTimestamp(),
UpdatedTime: upCred.UpdateTime.GetTimestamp(),
Version: 1,
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("user"),
PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(hm)),
PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(upHm)),
},
},
},
},
},
{
name: "success-prev-prefix",
id: credPrev.GetPublicId(),
name: "success-up-prev-prefix",
id: upCredPrev.GetPublicId(),
res: &pbs.GetCredentialResponse{
Item: &pb.Credential{
Id: credPrev.GetPublicId(),
CredentialStoreId: cred.GetStoreId(),
Id: upCredPrev.GetPublicId(),
CredentialStoreId: upCred.GetStoreId(),
Scope: &scopepb.ScopeInfo{Id: store.GetScopeId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()},
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
AuthorizedActions: testAuthorizedActions,
CreatedTime: credPrev.CreateTime.GetTimestamp(),
UpdatedTime: credPrev.UpdateTime.GetTimestamp(),
CreatedTime: upCredPrev.CreateTime.GetTimestamp(),
UpdatedTime: upCredPrev.UpdateTime.GetTimestamp(),
Version: 1,
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("user"),
PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(hm)),
PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(upHm)),
},
},
},
},
},
{
name: "success-spk",
id: spkCred.GetPublicId(),
res: &pbs.GetCredentialResponse{
Item: &pb.Credential{
Id: spkCred.GetPublicId(),
CredentialStoreId: spkCred.GetStoreId(),
Scope: &scopepb.ScopeInfo{Id: store.GetScopeId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()},
Type: credential.SshPrivateKeySubtype.String(),
AuthorizedActions: testAuthorizedActions,
CreatedTime: spkCred.CreateTime.GetTimestamp(),
UpdatedTime: spkCred.UpdateTime.GetTimestamp(),
Version: 1,
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String("user"),
PrivateKeyHmac: base64.RawURLEncoding.EncodeToString([]byte(spkHm)),
},
},
},
@@ -234,7 +279,7 @@ func TestGet(t *testing.T) {
},
{
name: "not found error",
id: fmt.Sprintf("%s_1234567890", static.UsernamePasswordCredentialPrefix),
id: fmt.Sprintf("%s_1234567890", credential.UsernamePasswordCredentialPrefix),
err: handlers.NotFoundError(),
},
{
@@ -286,10 +331,12 @@ func TestDelete(t *testing.T) {
_, prj := iam.TestScopes(t, iamRepo)
store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
cred := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId())
s, err := NewService(staticRepoFn, iamRepoFn)
require.NoError(t, err)
upCred := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId())
spkCred := static.TestSshPrivateKeyCredential(t, conn, wrapper, "user", static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId())
cases := []struct {
name string
id string
@@ -297,12 +344,16 @@ func TestDelete(t *testing.T) {
res *pbs.DeleteCredentialResponse
}{
{
name: "success",
id: cred.GetPublicId(),
name: "success-up",
id: upCred.GetPublicId(),
},
{
name: "success-spk",
id: spkCred.GetPublicId(),
},
{
name: "not found error",
id: fmt.Sprintf("%s_1234567890", static.UsernamePasswordCredentialPrefix),
id: fmt.Sprintf("%s_1234567890", credential.UsernamePasswordCredentialPrefix),
err: handlers.NotFoundError(),
},
{
@@ -357,8 +408,8 @@ func TestCreate(t *testing.T) {
name: "Can't specify Id",
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
Id: static.UsernamePasswordCredentialPrefix + "_notallowed",
Type: static.UsernamePasswordSubtype.String(),
Id: credential.UsernamePasswordCredentialPrefix + "_notallowed",
Type: credential.UsernamePasswordSubtype.String(),
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("username"),
@@ -373,7 +424,7 @@ func TestCreate(t *testing.T) {
name: "Invalid Credential Store Id",
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: "p_invalidid",
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("username"),
@@ -389,7 +440,7 @@ func TestCreate(t *testing.T) {
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
CreatedTime: timestamppb.Now(),
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("username"),
@@ -404,7 +455,7 @@ func TestCreate(t *testing.T) {
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
UpdatedTime: timestamppb.Now(),
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("username"),
@@ -433,7 +484,7 @@ func TestCreate(t *testing.T) {
name: "Must provide username",
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Password: wrapperspb.String("password"),
@@ -444,10 +495,10 @@ func TestCreate(t *testing.T) {
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Must provide password]",
name: "Must provide password",
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("username"),
@@ -458,10 +509,24 @@ func TestCreate(t *testing.T) {
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "valid",
name: "Must provide private key",
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
Type: static.UsernamePasswordSubtype.String(),
Type: credential.SshPrivateKeySubtype.String(),
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String("username"),
},
},
}},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "valid-up",
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
Type: credential.UsernamePasswordSubtype.String(),
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("username"),
@@ -469,9 +534,9 @@ func TestCreate(t *testing.T) {
},
},
}},
idPrefix: static.UsernamePasswordCredentialPrefix + "_",
idPrefix: credential.UsernamePasswordCredentialPrefix + "_",
res: &pbs.CreateCredentialResponse{
Uri: fmt.Sprintf("credentials/%s_", static.UsernamePasswordCredentialPrefix),
Uri: fmt.Sprintf("credentials/%s_", credential.UsernamePasswordCredentialPrefix),
Item: &pb.Credential{
Id: store.GetPublicId(),
CredentialStoreId: store.GetPublicId(),
@@ -479,7 +544,34 @@ func TestCreate(t *testing.T) {
UpdatedTime: store.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()},
Version: 1,
Type: static.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
AuthorizedActions: testAuthorizedActions,
},
},
},
{
name: "valid-spk",
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
Type: credential.SshPrivateKeySubtype.String(),
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String("username"),
PrivateKey: wrapperspb.String(static.TestSshPrivateKeyPem),
},
},
}},
idPrefix: credential.SshPrivateKeyCredentialPrefix + "_",
res: &pbs.CreateCredentialResponse{
Uri: fmt.Sprintf("credentials/%s_", credential.SshPrivateKeyCredentialPrefix),
Item: &pb.Credential{
Id: store.GetPublicId(),
CredentialStoreId: store.GetPublicId(),
CreatedTime: store.GetCreateTime().GetTimestamp(),
UpdatedTime: store.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()},
Version: 1,
Type: credential.SshPrivateKeySubtype.String(),
AuthorizedActions: testAuthorizedActions,
},
},
@@ -516,15 +608,33 @@ func TestCreate(t *testing.T) {
// Calculate hmac
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase)
password := tc.req.GetItem().GetUsernamePasswordAttributes().GetPassword().GetValue()
hm, err := crypto.HmacSha256(context.Background(), []byte(password), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519())
require.NoError(err)
// Validate attributes equal
assert.Equal(tc.req.GetItem().GetUsernamePasswordAttributes().GetUsername().GetValue(),
got.GetItem().GetUsernamePasswordAttributes().GetUsername().GetValue())
assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetUsernamePasswordAttributes().GetPasswordHmac())
assert.Empty(got.GetItem().GetUsernamePasswordAttributes().GetPassword())
switch tc.req.Item.Type {
case credential.UsernamePasswordSubtype.String():
password := tc.req.GetItem().GetUsernamePasswordAttributes().GetPassword().GetValue()
hm, err := crypto.HmacSha256(context.Background(), []byte(password), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519())
require.NoError(err)
// Validate attributes equal
assert.Equal(tc.req.GetItem().GetUsernamePasswordAttributes().GetUsername().GetValue(),
got.GetItem().GetUsernamePasswordAttributes().GetUsername().GetValue())
assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetUsernamePasswordAttributes().GetPasswordHmac())
assert.Empty(got.GetItem().GetUsernamePasswordAttributes().GetPassword())
case credential.SshPrivateKeySubtype.String():
hm, err := crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(err)
// Validate attributes equal
assert.Equal(tc.req.GetItem().GetSshPrivateKeyAttributes().GetUsername().GetValue(),
got.GetItem().GetSshPrivateKeyAttributes().GetUsername().GetValue())
assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyHmac())
assert.Empty(got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKey())
default:
require.Fail("unknown type")
}
// Clear attributes for compare below
got.Item.Attrs = nil
@@ -539,6 +649,16 @@ func TestCreate(t *testing.T) {
}
}
const TestSecondarySshPrivateKeyPem = `
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQAAAJDmpbfr5qW3
6wAAAAtzc2gtZWQyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQ
AAAEBvvkQkH06ad2GpX1VVARzu9NkHA6gzamAaQ/hkn5FuZvF/CEQBkquextDE6NUDc8WI
Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ=
-----END OPENSSH PRIVATE KEY-----
`
func TestUpdate(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
@@ -565,7 +685,7 @@ func TestUpdate(t *testing.T) {
store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
freshCred := func(user, pass string) (*static.UsernamePasswordCredential, func()) {
freshCredUp := func(user, pass string) (*static.UsernamePasswordCredential, func()) {
t.Helper()
cred := static.TestUsernamePasswordCredential(t, conn, wrapper, user, pass, store.GetPublicId(), prj.GetPublicId())
clean := func() {
@@ -575,6 +695,16 @@ func TestUpdate(t *testing.T) {
return cred, clean
}
freshCredSpk := func(user string) (*static.SshPrivateKeyCredential, func()) {
t.Helper()
cred := static.TestSshPrivateKeyCredential(t, conn, wrapper, user, static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId())
clean := func() {
_, err := s.DeleteCredential(ctx, &pbs.DeleteCredentialRequest{Id: cred.GetPublicId()})
require.NoError(t, err)
}
return cred, clean
}
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase)
require.NoError(t, err)
@@ -628,7 +758,23 @@ func TestUpdate(t *testing.T) {
},
},
{
name: "update-username",
name: "name-and-description-spk",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("name", "description"),
Item: &pb.Credential{
Name: wrapperspb.String("new-name"),
Description: wrapperspb.String("new-description"),
},
},
res: func(in *pb.Credential) *pb.Credential {
out := proto.Clone(in).(*pb.Credential)
out.Name = wrapperspb.String("new-name")
out.Description = wrapperspb.String("new-description")
return out
},
},
{
name: "update-username-up",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("attributes.username"),
Item: &pb.Credential{
@@ -645,6 +791,24 @@ func TestUpdate(t *testing.T) {
return out
},
},
{
name: "update-username-spk",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("attributes.username"),
Item: &pb.Credential{
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String("new-user-name"),
},
},
},
},
res: func(in *pb.Credential) *pb.Credential {
out := proto.Clone(in).(*pb.Credential)
out.GetSshPrivateKeyAttributes().Username = wrapperspb.String("new-user-name")
return out
},
},
{
name: "update-password",
req: &pbs.UpdateCredentialRequest{
@@ -665,6 +829,26 @@ func TestUpdate(t *testing.T) {
return out
},
},
{
name: "update-spk",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("attributes.private_key"),
Item: &pb.Credential{
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
PrivateKey: wrapperspb.String(TestSecondarySshPrivateKeyPem),
},
},
},
},
res: func(in *pb.Credential) *pb.Credential {
out := proto.Clone(in).(*pb.Credential)
hm, err := crypto.HmacSha256(context.Background(), []byte(TestSecondarySshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm))
return out
},
},
{
name: "update-username-and-password",
req: &pbs.UpdateCredentialRequest{
@@ -689,12 +873,43 @@ func TestUpdate(t *testing.T) {
return out
},
},
{
name: "update-username-and-spk",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("attributes.username", "attributes.private_key"),
Item: &pb.Credential{
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String("new-username"),
PrivateKey: wrapperspb.String(TestSecondarySshPrivateKeyPem),
},
},
},
},
res: func(in *pb.Credential) *pb.Credential {
out := proto.Clone(in).(*pb.Credential)
out.GetSshPrivateKeyAttributes().Username = wrapperspb.String("new-username")
hm, err := crypto.HmacSha256(context.Background(), []byte(TestSecondarySshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm))
return out
},
},
}
for _, tc := range successCases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
cred, cleanup := freshCred("user", "pass")
var cred credential.Static
var cleanup func()
switch {
case strings.Contains(tc.name, "spk"):
cred, cleanup = freshCredSpk("user")
default:
cred, cleanup = freshCredUp("user", "pass")
}
defer cleanup()
if tc.req.Item.GetVersion() == 0 {
@@ -723,58 +938,60 @@ func TestUpdate(t *testing.T) {
assert.Empty(cmp.Diff(got, want, protocmp.Transform()))
})
}
/*
// cant update read only fields
cred, cleanup := freshCred("user", "pass")
defer cleanup()
// cant update read only fields
cred, cleanup := freshCred("user", "pass")
defer cleanup()
newStore := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
newStore := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
roCases := []struct {
path string
item *pb.Credential
matcher func(t *testing.T, e error) // When not set defaults to checking against InvalidArgument Error
}{
{
path: "type",
item: &pb.Credential{Type: "something"},
},
{
path: "store_id",
item: &pb.Credential{CredentialStoreId: newStore.GetPublicId()},
},
{
path: "updated_time",
item: &pb.Credential{UpdatedTime: timestamppb.Now()},
},
{
path: "created_time",
item: &pb.Credential{UpdatedTime: timestamppb.Now()},
},
{
path: "authorized_actions",
item: &pb.Credential{AuthorizedActions: append(testAuthorizedActions, "another")},
},
}
for _, tc := range roCases {
t.Run(fmt.Sprintf("ReadOnlyField/%s", tc.path), func(t *testing.T) {
req := &pbs.UpdateCredentialRequest{
Id: cred.GetPublicId(),
Item: tc.item,
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}},
}
req.Item.Version = cred.Version
got, gErr := s.UpdateCredential(ctx, req)
assert.Error(t, gErr)
matcher := tc.matcher
if matcher == nil {
matcher = func(t *testing.T, e error) {
assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr)
roCases := []struct {
path string
item *pb.Credential
matcher func(t *testing.T, e error) // When not set defaults to checking against InvalidArgument Error
}{
{
path: "type",
item: &pb.Credential{Type: "something"},
},
{
path: "store_id",
item: &pb.Credential{CredentialStoreId: newStore.GetPublicId()},
},
{
path: "updated_time",
item: &pb.Credential{UpdatedTime: timestamppb.Now()},
},
{
path: "created_time",
item: &pb.Credential{UpdatedTime: timestamppb.Now()},
},
{
path: "authorized_actions",
item: &pb.Credential{AuthorizedActions: append(testAuthorizedActions, "another")},
},
}
for _, tc := range roCases {
t.Run(fmt.Sprintf("ReadOnlyField/%s", tc.path), func(t *testing.T) {
req := &pbs.UpdateCredentialRequest{
Id: cred.GetPublicId(),
Item: tc.item,
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}},
}
}
matcher(t, gErr)
assert.Nil(t, got)
})
}
req.Item.Version = cred.Version
got, gErr := s.UpdateCredential(ctx, req)
assert.Error(t, gErr)
matcher := tc.matcher
if matcher == nil {
matcher = func(t *testing.T, e error) {
assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr)
}
}
matcher(t, gErr)
assert.Nil(t, got)
})
}
*/
}
@@ -12,7 +12,6 @@ import (
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/credential"
credstatic "github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/credential/vault"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
@@ -1797,13 +1796,13 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ
badFields[globals.EgressCredentialSourceIdsField] = "Application or Egress Credential Source IDs must be provided."
}
for _, cl := range req.GetApplicationCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credstatic.UsernamePasswordCredentialPrefix, credstatic.PreviousUsernamePasswordCredentialPrefix) {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) {
badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
}
for _, cl := range req.GetEgressCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credstatic.UsernamePasswordCredentialPrefix, credstatic.PreviousUsernamePasswordCredentialPrefix) {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) {
badFields[globals.EgressCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
@@ -1823,13 +1822,13 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ
badFields[globals.VersionField] = "Required field."
}
for _, cl := range req.GetApplicationCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credstatic.UsernamePasswordCredentialPrefix, credstatic.PreviousUsernamePasswordCredentialPrefix) {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) {
badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
}
for _, cl := range req.GetEgressCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credstatic.UsernamePasswordCredentialPrefix, credstatic.PreviousUsernamePasswordCredentialPrefix) {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) {
badFields[globals.EgressCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
@@ -1853,13 +1852,13 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc
badFields[globals.EgressCredentialSourceIdsField] = "Application or Egress Credential Source IDs must be provided."
}
for _, cl := range req.GetApplicationCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credstatic.UsernamePasswordCredentialPrefix, credstatic.PreviousUsernamePasswordCredentialPrefix) {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) {
badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
}
for _, cl := range req.GetEgressCredentialSourceIds() {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credstatic.UsernamePasswordCredentialPrefix, credstatic.PreviousUsernamePasswordCredentialPrefix) {
if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix) {
badFields[globals.EgressCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl)
break
}
@@ -2734,7 +2734,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) {
require.NoError(t, err)
credResp, err := credService.CreateCredential(ctx, &pbs.CreateCredentialRequest{Item: &credpb.Credential{
CredentialStoreId: staticStore.GetPublicId(),
Type: credstatic.UsernamePasswordSubtype.String(),
Type: credential.UsernamePasswordSubtype.String(),
Name: wrapperspb.String("Cred Name"),
Description: wrapperspb.String("Cred Description"),
Attrs: &credpb.Credential_UsernamePasswordAttributes{
@@ -1,5 +1,6 @@
begin;
-- updated in 39/01_static_ssh_private_key_creds
-- drop constraint so we can migrate user_password to username_password
alter table credential_type_enm
drop constraint only_predefined_credential_types_allowed;
@@ -0,0 +1,91 @@
begin;
-- drop constraint so we can add ssh_private_key
alter table credential_type_enm
drop constraint only_predefined_credential_types_allowed;
-- Add new constraint that only allows known types
-- This replaces the constraint defined in 36/01_credential_type_up
alter table credential_type_enm
add constraint only_predefined_credential_types_allowed
check (
name in (
'unspecified',
'username_password',
'ssh_private_key'
)
);
create table credential_static_ssh_private_key_credential (
public_id wt_public_id primary key,
store_id wt_public_id not null
constraint credential_static_store_fkey
references credential_static_store (public_id)
on delete cascade
on update cascade,
name wt_name,
description wt_description,
create_time wt_timestamp,
update_time wt_timestamp,
version wt_version,
username text not null
constraint username_must_not_be_empty
check(length(trim(username)) > 0),
private_key_encrypted bytea not null
constraint private_key_encrypted_must_not_be_empty
check(length(private_key_encrypted) > 0),
private_key_hmac bytea not null
constraint ssh_private_key_hmac_must_not_be_empty
check(length(private_key_hmac) > 0),
key_id text not null
constraint kms_data_key_version_fkey
references kms_data_key_version (private_id)
on delete restrict
on update cascade,
constraint credential_static_fkey
foreign key (store_id, public_id)
references credential_static (store_id, public_id)
on delete cascade
on update cascade,
constraint credential_static_ssh_private_key_credential_store_id_name_uq
unique(store_id, name),
-- The name of this constraint does not follow our naming conventions for
-- unique constraints because it would be to long. The max length for
-- identifiers in PostgreSQL is 63 characters.
-- credential_static_ssh_private_key_credential_store_id_public_id_uq
-- is 67 characters.
--
-- https://www.postgresql.org/docs/current/limits.html
constraint credential_static_ssh_private_key_store_id_public_id_uq
unique(store_id, public_id)
);
comment on table credential_static_ssh_private_key_credential is
'credential_static_ssh_private_key_credential is a table where each row is a resource that represents a static ssh private key credential. '
'It is a credential_static subtype and an aggregate root.';
create trigger update_version_column after update on credential_static_ssh_private_key_credential
for each row execute procedure update_version_column();
create trigger update_time_column before update on credential_static_ssh_private_key_credential
for each row execute procedure update_time_column();
create trigger default_create_time_column before insert on credential_static_ssh_private_key_credential
for each row execute procedure default_create_time();
create trigger immutable_columns before update on credential_static_ssh_private_key_credential
for each row execute procedure immutable_columns('public_id', 'store_id','create_time');
create trigger insert_credential_static_subtype before insert on credential_static_ssh_private_key_credential
for each row execute procedure insert_credential_static_subtype();
create trigger delete_credential_static_subtype after delete on credential_static_ssh_private_key_credential
for each row execute procedure delete_credential_static_subtype();
insert into oplog_ticket (name, version)
values
('credential_static_ssh_private_key_credential', 1);
commit;
@@ -64,6 +64,11 @@ message Credential {
(custom_options.v1.generate_sdk_option) = true,
(custom_options.v1.subtype) = "username_password"
];
SshPrivateKeyAttributes ssh_private_key_attributes = 102 [
(google.api.field_visibility).restriction = "INTERNAL",
(custom_options.v1.generate_sdk_option) = true,
(custom_options.v1.subtype) = "ssh_private_key"
];
}
// Output only. The available actions on this resource for this user.
@@ -100,3 +105,34 @@ message UsernamePasswordAttributes {
}
]; // @gotags: `class:"public"`
}
// The attributes of a SshPrivateKey Credential.
message SshPrivateKeyAttributes {
// The username associated with the credential.
google.protobuf.StringValue username = 10 [
(custom_options.v1.generate_sdk_option) = true,
(custom_options.v1.mask_mapping) = {
this: "attributes.username"
that: "Username"
}
]; // @gotags: `class:"public"`
// Input only. The SSH private key associated with the credential.
google.protobuf.StringValue private_key = 20 [
json_name = "private_key",
(custom_options.v1.generate_sdk_option) = true,
(custom_options.v1.mask_mapping) = {
this: "attributes.private_key"
that: "PrivateKey"
}
]; // @gotags: `class:"secret"`
// Output only. The hmac value of the SSH private key.
string private_key_hmac = 30 [
json_name = "private_key_hmac",
(custom_options.v1.mask_mapping) = {
this: "attributes.private_key_hmac"
that: "PrivateKeyHmac"
}
]; // @gotags: `class:"public"`
}
@@ -116,3 +116,74 @@ message UsernamePasswordCredential {
// @inject_tag: `gorm:"not_null"`
string key_id = 12;
}
message SshPrivateKeyCredential {
// 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 scope_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;
// username is the username associated with the credential.
// It must be set.
// @inject_tag: `gorm:"not_null"`
string username = 8 [(custom_options.v1.mask_mapping) = {
this: "Username"
that: "attributes.username"
}];
// private_key is the plain-text of the private key associated with the
// credential. We are not storing this plain-text private key in the database.
// @inject_tag: `gorm:"-" wrapping:"pt,private_key"`
bytes private_key = 9 [(custom_options.v1.mask_mapping) = {
this: "PrivateKey"
that: "attributes.private_key"
}];
// private_key_encrypted is the ciphertext of the private key. It is stored in
// the database.
// @inject_tag: `gorm:"column:private_key_encrypted;not_null" wrapping:"ct,private_key"`
bytes private_key_encrypted = 10;
// private_key_hmac is a sha256-hmac of the unencrypted private key. It is recalculated
// everytime the private key is updated.
// @inject_tag: `gorm:"not_null"`
bytes private_key_hmac = 11 [(custom_options.v1.mask_mapping) = {
this: "PrivateKeyHmac"
that: "attributes.private_key_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 = 12;
}
@@ -8,7 +8,7 @@ import (
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/credentials"
"github.com/hashicorp/boundary/api/credentialstores"
"github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/daemon/controller"
"github.com/hashicorp/boundary/internal/iam"
"github.com/stretchr/testify/assert"
@@ -35,7 +35,7 @@ func TestList(t *testing.T) {
require.NoError(err)
assert.Empty(ul.Items)
cred, err := credClient.Create(tc.Context(), static.UsernamePasswordSubtype.String(), cs.Item.Id,
cred, err := credClient.Create(tc.Context(), credential.UsernamePasswordSubtype.String(), cs.Item.Id,
credentials.WithName("0"),
credentials.WithDescription("description"),
credentials.WithUsernamePasswordCredentialUsername("user"),
@@ -50,7 +50,7 @@ func TestList(t *testing.T) {
assert.ElementsMatch(comparableCatalogSlice(expected[:1]), comparableCatalogSlice(ul.Items))
for i := 1; i < 10; i++ {
cred, err := credClient.Create(tc.Context(), static.UsernamePasswordSubtype.String(), cs.Item.Id,
cred, err := credClient.Create(tc.Context(), credential.UsernamePasswordSubtype.String(), cs.Item.Id,
credentials.WithName(fmt.Sprintf("%d", i)),
credentials.WithDescription("description"),
credentials.WithUsernamePasswordCredentialUsername("user"),
@@ -110,7 +110,7 @@ func TestCrud(t *testing.T) {
}
credClient := credentials.NewClient(client)
cred, err := credClient.Create(tc.Context(), static.UsernamePasswordSubtype.String(), cs.Item.Id, credentials.WithName("foo"),
cred, err := credClient.Create(tc.Context(), credential.UsernamePasswordSubtype.String(), cs.Item.Id, credentials.WithName("foo"),
credentials.WithUsernamePasswordCredentialUsername("user"), credentials.WithUsernamePasswordCredentialPassword("pass"))
require.NoError(err)
require.NotNil(cs)
@@ -167,7 +167,7 @@ func TestErrors(t *testing.T) {
c := credentials.NewClient(client)
cred, err := c.Create(tc.Context(), static.UsernamePasswordSubtype.String(), cs.Item.Id, credentials.WithName("foo"),
cred, err := c.Create(tc.Context(), credential.UsernamePasswordSubtype.String(), cs.Item.Id, credentials.WithName("foo"),
credentials.WithUsernamePasswordCredentialUsername("user"), credentials.WithUsernamePasswordCredentialPassword("pass"))
require.NoError(err)
require.NotNil(cred)
@@ -189,12 +189,12 @@ func TestErrors(t *testing.T) {
assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode())
// Same name
_, err = c.Create(tc.Context(), static.UsernamePasswordSubtype.String(), proj.GetPublicId(), credentials.WithName("foo"))
_, err = c.Create(tc.Context(), credential.UsernamePasswordSubtype.String(), proj.GetPublicId(), credentials.WithName("foo"))
require.Error(err)
apiErr = api.AsServerError(err)
assert.NotNil(apiErr)
_, err = c.Read(tc.Context(), static.UsernamePasswordCredentialPrefix+"_doesntexis")
_, err = c.Read(tc.Context(), credential.UsernamePasswordCredentialPrefix+"_doesntexis")
require.Error(err)
apiErr = api.AsServerError(err)
assert.NotNil(apiErr)
@@ -54,6 +54,7 @@ type Credential struct {
// Types that are assignable to Attrs:
// *Credential_Attributes
// *Credential_UsernamePasswordAttributes
// *Credential_SshPrivateKeyAttributes
Attrs isCredential_Attrs `protobuf_oneof:"attrs"`
// Output only. The available actions on this resource for this user.
AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty" class:"public"` // @gotags: `class:"public"`
@@ -175,6 +176,13 @@ func (x *Credential) GetUsernamePasswordAttributes() *UsernamePasswordAttributes
return nil
}
func (x *Credential) GetSshPrivateKeyAttributes() *SshPrivateKeyAttributes {
if x, ok := x.GetAttrs().(*Credential_SshPrivateKeyAttributes); ok {
return x.SshPrivateKeyAttributes
}
return nil
}
func (x *Credential) GetAuthorizedActions() []string {
if x != nil {
return x.AuthorizedActions
@@ -195,10 +203,16 @@ type Credential_UsernamePasswordAttributes struct {
UsernamePasswordAttributes *UsernamePasswordAttributes `protobuf:"bytes,101,opt,name=username_password_attributes,json=usernamePasswordAttributes,proto3,oneof"`
}
type Credential_SshPrivateKeyAttributes struct {
SshPrivateKeyAttributes *SshPrivateKeyAttributes `protobuf:"bytes,102,opt,name=ssh_private_key_attributes,json=sshPrivateKeyAttributes,proto3,oneof"`
}
func (*Credential_Attributes) isCredential_Attrs() {}
func (*Credential_UsernamePasswordAttributes) isCredential_Attrs() {}
func (*Credential_SshPrivateKeyAttributes) isCredential_Attrs() {}
// The attributes of a UsernamePassword Credential.
type UsernamePasswordAttributes struct {
state protoimpl.MessageState
@@ -266,6 +280,73 @@ func (x *UsernamePasswordAttributes) GetPasswordHmac() string {
return ""
}
// The attributes of a SshPrivateKey Credential.
type SshPrivateKeyAttributes struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The username associated with the credential.
Username *wrapperspb.StringValue `protobuf:"bytes,10,opt,name=username,proto3" json:"username,omitempty" class:"public"` // @gotags: `class:"public"`
// Input only. The SSH private key associated with the credential.
PrivateKey *wrapperspb.StringValue `protobuf:"bytes,20,opt,name=private_key,proto3" json:"private_key,omitempty" class:"secret"` // @gotags: `class:"secret"`
// Output only. The hmac value of the SSH private key.
PrivateKeyHmac string `protobuf:"bytes,30,opt,name=private_key_hmac,proto3" json:"private_key_hmac,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *SshPrivateKeyAttributes) Reset() {
*x = SshPrivateKeyAttributes{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SshPrivateKeyAttributes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SshPrivateKeyAttributes) ProtoMessage() {}
func (x *SshPrivateKeyAttributes) ProtoReflect() protoreflect.Message {
mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SshPrivateKeyAttributes.ProtoReflect.Descriptor instead.
func (*SshPrivateKeyAttributes) Descriptor() ([]byte, []int) {
return file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP(), []int{2}
}
func (x *SshPrivateKeyAttributes) GetUsername() *wrapperspb.StringValue {
if x != nil {
return x.Username
}
return nil
}
func (x *SshPrivateKeyAttributes) GetPrivateKey() *wrapperspb.StringValue {
if x != nil {
return x.PrivateKey
}
return nil
}
func (x *SshPrivateKeyAttributes) GetPrivateKeyHmac() string {
if x != nil {
return x.PrivateKeyHmac
}
return ""
}
var File_controller_api_resources_credentials_v1_credential_proto protoreflect.FileDescriptor
var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []byte{
@@ -288,7 +369,7 @@ var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []by
0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61,
0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa8, 0x06, 0x0a, 0x0a,
0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd3, 0x07, 0x0a, 0x0a,
0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x63, 0x72,
0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69,
@@ -335,37 +416,68 @@ var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []by
0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0xfa, 0xd2,
0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00,
0x52, 0x1a, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12,
0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f,
0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a,
0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, 0xb6, 0x02, 0x0a, 0x1a, 0x55, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69,
0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1f, 0x0a,
0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72,
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08,
0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x61, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73,
0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72,
0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd,
0x29, 0x1f, 0x0a, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72,
0x64, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x52, 0x0a, 0x0d, 0x70,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x1e, 0x20, 0x01,
0x28, 0x09, 0x42, 0x2c, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62,
0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d,
0x61, 0x63, 0x12, 0x0c, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63,
0x52, 0x0d, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x42,
0x58, 0x5a, 0x56, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61,
0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79,
0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x73, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x3b, 0x63, 0x72,
0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
0x72, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xa8, 0x01, 0x0a,
0x1a, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79,
0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x66, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x40, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x72, 0x65,
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x73, 0x68, 0x50,
0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75,
0x74, 0x65, 0x73, 0x42, 0x27, 0xa0, 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x0f, 0x73, 0x73, 0x68,
0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0xfa, 0xd2, 0xe4, 0x93,
0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x17,
0x73, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74,
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f,
0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02,
0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64,
0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72,
0x73, 0x22, 0xb6, 0x02, 0x0a, 0x1a, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73,
0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x42, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1f, 0x0a, 0x13, 0x61, 0x74, 0x74, 0x72,
0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x61, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18,
0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x42, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1f, 0x0a, 0x13, 0x61,
0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x12, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x08, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x52, 0x0a, 0x0d, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2c, 0xc2,
0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e,
0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x0c, 0x50,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63, 0x52, 0x0d, 0x70, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x22, 0xc9, 0x02, 0x0a, 0x17, 0x53,
0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72,
0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1f,
0x0a, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65,
0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52,
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x6c, 0x0a, 0x0b, 0x70, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2c, 0xa0, 0xda,
0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x0a,
0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x76,
0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x5d, 0x0a, 0x10, 0x70, 0x72, 0x69, 0x76, 0x61,
0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x1e, 0x20, 0x01, 0x28,
0x09, 0x42, 0x31, 0xc2, 0xdd, 0x29, 0x2d, 0x0a, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75,
0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f,
0x68, 0x6d, 0x61, 0x63, 0x12, 0x0e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79,
0x48, 0x6d, 0x61, 0x63, 0x52, 0x10, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65,
0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x42, 0x58, 0x5a, 0x56, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62,
0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f,
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74,
0x69, 0x61, 0x6c, 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -380,30 +492,34 @@ func file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP()
return file_controller_api_resources_credentials_v1_credential_proto_rawDescData
}
var file_controller_api_resources_credentials_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_controller_api_resources_credentials_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_controller_api_resources_credentials_v1_credential_proto_goTypes = []interface{}{
(*Credential)(nil), // 0: controller.api.resources.credentials.v1.Credential
(*UsernamePasswordAttributes)(nil), // 1: controller.api.resources.credentials.v1.UsernamePasswordAttributes
(*scopes.ScopeInfo)(nil), // 2: controller.api.resources.scopes.v1.ScopeInfo
(*wrapperspb.StringValue)(nil), // 3: google.protobuf.StringValue
(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
(*structpb.Struct)(nil), // 5: google.protobuf.Struct
(*SshPrivateKeyAttributes)(nil), // 2: controller.api.resources.credentials.v1.SshPrivateKeyAttributes
(*scopes.ScopeInfo)(nil), // 3: controller.api.resources.scopes.v1.ScopeInfo
(*wrapperspb.StringValue)(nil), // 4: google.protobuf.StringValue
(*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp
(*structpb.Struct)(nil), // 6: google.protobuf.Struct
}
var file_controller_api_resources_credentials_v1_credential_proto_depIdxs = []int32{
2, // 0: controller.api.resources.credentials.v1.Credential.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo
3, // 1: controller.api.resources.credentials.v1.Credential.name:type_name -> google.protobuf.StringValue
3, // 2: controller.api.resources.credentials.v1.Credential.description:type_name -> google.protobuf.StringValue
4, // 3: controller.api.resources.credentials.v1.Credential.created_time:type_name -> google.protobuf.Timestamp
4, // 4: controller.api.resources.credentials.v1.Credential.updated_time:type_name -> google.protobuf.Timestamp
5, // 5: controller.api.resources.credentials.v1.Credential.attributes:type_name -> google.protobuf.Struct
1, // 6: controller.api.resources.credentials.v1.Credential.username_password_attributes:type_name -> controller.api.resources.credentials.v1.UsernamePasswordAttributes
3, // 7: controller.api.resources.credentials.v1.UsernamePasswordAttributes.username:type_name -> google.protobuf.StringValue
3, // 8: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue
9, // [9:9] is the sub-list for method output_type
9, // [9:9] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
3, // 0: controller.api.resources.credentials.v1.Credential.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo
4, // 1: controller.api.resources.credentials.v1.Credential.name:type_name -> google.protobuf.StringValue
4, // 2: controller.api.resources.credentials.v1.Credential.description:type_name -> google.protobuf.StringValue
5, // 3: controller.api.resources.credentials.v1.Credential.created_time:type_name -> google.protobuf.Timestamp
5, // 4: controller.api.resources.credentials.v1.Credential.updated_time:type_name -> google.protobuf.Timestamp
6, // 5: controller.api.resources.credentials.v1.Credential.attributes:type_name -> google.protobuf.Struct
1, // 6: controller.api.resources.credentials.v1.Credential.username_password_attributes:type_name -> controller.api.resources.credentials.v1.UsernamePasswordAttributes
2, // 7: controller.api.resources.credentials.v1.Credential.ssh_private_key_attributes:type_name -> controller.api.resources.credentials.v1.SshPrivateKeyAttributes
4, // 8: controller.api.resources.credentials.v1.UsernamePasswordAttributes.username:type_name -> google.protobuf.StringValue
4, // 9: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue
4, // 10: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.username:type_name -> google.protobuf.StringValue
4, // 11: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key:type_name -> google.protobuf.StringValue
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_api_resources_credentials_v1_credential_proto_init() }
@@ -436,10 +552,23 @@ func file_controller_api_resources_credentials_v1_credential_proto_init() {
return nil
}
}
file_controller_api_resources_credentials_v1_credential_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SshPrivateKeyAttributes); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_controller_api_resources_credentials_v1_credential_proto_msgTypes[0].OneofWrappers = []interface{}{
(*Credential_Attributes)(nil),
(*Credential_UsernamePasswordAttributes)(nil),
(*Credential_SshPrivateKeyAttributes)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -447,7 +576,7 @@ func file_controller_api_resources_credentials_v1_credential_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_api_resources_credentials_v1_credential_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},