feat(credential): Use credential type when issuing credentials

This change uses the credential type attribute when issuing credentials
including mapping overrides.
pull/1796/head
Michael Gaffney 5 years ago committed by Timothy Messier
parent 17762ed01e
commit 0e637f47b5
No known key found for this signature in database
GPG Key ID: EFD2F184F7600572

@ -32,9 +32,7 @@ const (
type Library interface {
boundary.Resource
GetStoreId() string
// TODO(mgaffney) 10/2021: Add method for returning the credential type
// of the library
CredentialType() Type
}
// Purpose is the purpose of the credential.

@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/db/timestamp"
@ -12,84 +13,153 @@ import (
"github.com/hashicorp/boundary/internal/kms"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-kms-wrapping/structwrapping"
vault "github.com/hashicorp/vault/api"
"google.golang.org/protobuf/proto"
)
var _ credential.Dynamic = (*actualCredential)(nil)
var _ credential.UserPassword = (*usrPassCred)(nil)
type usrPassCred struct {
*baseCred
username string
password credential.Password
}
func (c *usrPassCred) Username() string { return c.username }
func (c *usrPassCred) Password() credential.Password { return c.password }
var _ credential.Dynamic = (*baseCred)(nil)
type baseCred struct {
*Credential
type actualCredential struct {
id string
sessionId string
lib *privateLibrary
secretData map[string]interface{}
purpose credential.Purpose
}
func (ac *actualCredential) GetPublicId() string { return ac.id }
func (ac *actualCredential) GetSessionId() string { return ac.sessionId }
func (ac *actualCredential) Secret() credential.SecretData { return ac.secretData }
func (ac *actualCredential) Library() credential.Library { return ac.lib }
func (ac *actualCredential) Purpose() credential.Purpose { return ac.purpose }
func (bc *baseCred) Secret() credential.SecretData { return bc.secretData }
func (bc *baseCred) Library() credential.Library { return bc.lib }
func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.Purpose }
func (bc *baseCred) getExpiration() time.Duration { return bc.expiration }
// convert converts bc to a specific credential type if bc is not
// UnspecifiedType.
func convert(ctx context.Context, bc *baseCred) (dynamicCred, error) {
switch bc.Library().CredentialType() {
case credential.UserPasswordType:
return baseToUsrPass(ctx, bc)
}
return bc, nil
}
func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) {
switch {
case bc == nil:
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred"))
case bc.lib == nil:
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred.lib"))
case bc.Library().CredentialType() != credential.UserPasswordType:
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type"))
}
uAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PasswordAttribute
if uAttr == "" {
uAttr = "username"
}
if pAttr == "" {
pAttr = "password"
}
var username, password string
if u, ok := bc.secretData[uAttr]; ok {
if u, ok := u.(string); ok {
username = u
}
}
if p, ok := bc.secretData[pAttr]; ok {
if p, ok := p.(string); ok {
password = p
}
}
if username == "" || password == "" {
return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping))
}
return &usrPassCred{
baseCred: bc,
username: username,
password: credential.Password(password),
}, nil
}
var _ credential.Library = (*privateLibrary)(nil)
// A privateLibrary contains all the values needed to connect to Vault and
// retrieve credentials.
type privateLibrary struct {
PublicId string `gorm:"primary_key"`
StoreId string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
ScopeId string
VaultPath string
HttpMethod string
HttpRequestBody []byte
VaultAddress string
Namespace string
CaCert []byte
TlsServerName string
TlsSkipVerify bool
TokenHmac []byte
Token TokenSecret
CtToken []byte
TokenKeyId string
ClientCert []byte
ClientKey KeySecret
CtClientKey []byte
ClientKeyId string
Purpose credential.Purpose `gorm:"-"`
PublicId string `gorm:"primary_key"`
StoreId string
CredType string `gorm:"column:credential_type"`
UsernameAttribute string
PasswordAttribute string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
ScopeId string
VaultPath string
HttpMethod string
HttpRequestBody []byte
VaultAddress string
Namespace string
CaCert []byte
TlsServerName string
TlsSkipVerify bool
TokenHmac []byte
Token TokenSecret
CtToken []byte
TokenKeyId string
ClientCert []byte
ClientKey KeySecret
CtClientKey []byte
ClientKeyId string
Purpose credential.Purpose `gorm:"-"`
}
func (pl *privateLibrary) clone() *privateLibrary {
// The 'append(a[:0:0], a...)' comes from
// https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F
return &privateLibrary{
PublicId: pl.PublicId,
StoreId: pl.StoreId,
Name: pl.Name,
Description: pl.Description,
CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp),
UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp),
Version: pl.Version,
ScopeId: pl.ScopeId,
VaultPath: pl.VaultPath,
HttpMethod: pl.HttpMethod,
HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...),
VaultAddress: pl.VaultAddress,
Namespace: pl.Namespace,
CaCert: append(pl.CaCert[:0:0], pl.CaCert...),
TlsServerName: pl.TlsServerName,
TlsSkipVerify: pl.TlsSkipVerify,
TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...),
Token: append(pl.Token[:0:0], pl.Token...),
CtToken: append(pl.CtToken[:0:0], pl.CtToken...),
TokenKeyId: pl.TokenKeyId,
ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...),
ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...),
CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...),
ClientKeyId: pl.ClientKeyId,
Purpose: pl.Purpose,
PublicId: pl.PublicId,
StoreId: pl.StoreId,
CredType: pl.CredType,
UsernameAttribute: pl.UsernameAttribute,
PasswordAttribute: pl.PasswordAttribute,
Name: pl.Name,
Description: pl.Description,
CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp),
UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp),
Version: pl.Version,
ScopeId: pl.ScopeId,
VaultPath: pl.VaultPath,
HttpMethod: pl.HttpMethod,
HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...),
VaultAddress: pl.VaultAddress,
Namespace: pl.Namespace,
CaCert: append(pl.CaCert[:0:0], pl.CaCert...),
TlsServerName: pl.TlsServerName,
TlsSkipVerify: pl.TlsSkipVerify,
TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...),
Token: append(pl.Token[:0:0], pl.Token...),
CtToken: append(pl.CtToken[:0:0], pl.CtToken...),
TokenKeyId: pl.TokenKeyId,
ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...),
ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...),
CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...),
ClientKeyId: pl.ClientKeyId,
Purpose: pl.Purpose,
}
}
@ -101,6 +171,15 @@ func (pl *privateLibrary) GetVersion() uint32 { return pl.Versi
func (pl *privateLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime }
func (pl *privateLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime }
func (pl *privateLibrary) CredentialType() credential.Type {
switch ct := pl.CredType; ct {
case "":
return credential.UnspecifiedType
default:
return credential.Type(ct)
}
}
func (pl *privateLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "vault.(privateLibrary).decrypt"
@ -157,6 +236,63 @@ func (pl *privateLibrary) client() (*client, error) {
return client, nil
}
type dynamicCred interface {
credential.Dynamic
getExpiration() time.Duration
insertQuery() (query string, queryValues []interface{})
updateSessionQuery(purpose credential.Purpose) (query string, queryValues []interface{})
}
// retrieveCredential retrieves a dynamic credential from Vault for the
// given sessionId.
func (pl *privateLibrary) retrieveCredential(ctx context.Context, op errors.Op, sessionId string) (dynamicCred, error) {
// Get the credential ID early. No need to get a secret from Vault
// if there is no way to save it in the database.
credId, err := newCredentialId()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
client, err := pl.client()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var secret *vault.Secret
switch Method(pl.HttpMethod) {
case MethodGet:
secret, err = client.get(pl.VaultPath)
case MethodPost:
secret, err = client.post(pl.VaultPath, pl.HttpRequestBody)
default:
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unknown http method: library: %s", pl.PublicId))
}
if err != nil {
// TODO(mgaffney) 05/2021: detect if the error is because of an
// expired or invalid token
return nil, errors.Wrap(ctx, err, op)
}
if secret == nil {
return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op))
}
leaseDuration := time.Duration(secret.LeaseDuration) * time.Second
cred, err := newCredential(pl.GetPublicId(), sessionId, secret.LeaseID, pl.TokenHmac, leaseDuration)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
cred.PublicId = credId
cred.IsRenewable = secret.Renewable
dCred := &baseCred{
Credential: cred,
lib: pl,
secretData: secret.Data,
}
return convert(ctx, dCred)
}
// TableName returns the table name for gorm.
func (pl *privateLibrary) TableName() string {
return "credential_vault_library_private"

@ -6,6 +6,7 @@ import (
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/scheduler"
@ -77,7 +78,7 @@ func TestRepository_getPrivateLibraries(t *testing.T) {
assert.NotNil(origLookup.Token())
assert.Equal(origStore.GetPublicId(), origLookup.GetPublicId())
libs := make(map[string]*CredentialLibrary, 3)
libs := make(map[string]*CredentialLibrary)
var requests []credential.Request
{
libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path")
@ -112,6 +113,72 @@ func TestRepository_getPrivateLibraries(t *testing.T) {
req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose}
requests = append(requests, req)
}
{
opts := []Option{
WithCredentialType(credential.UserPasswordType),
}
libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...)
assert.NoError(err)
require.NotNil(libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(err)
require.NotNil(lib)
libs[lib.GetPublicId()] = lib
req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose}
requests = append(requests, req)
}
{
opts := []Option{
WithCredentialType(credential.UserPasswordType),
WithMappingOverride(NewUserPasswordOverride(
WithOverrideUsernameAttribute("test-username"),
)),
}
libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...)
assert.NoError(err)
require.NotNil(libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(err)
require.NotNil(lib)
libs[lib.GetPublicId()] = lib
req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose}
requests = append(requests, req)
}
{
opts := []Option{
WithCredentialType(credential.UserPasswordType),
WithMappingOverride(NewUserPasswordOverride(
WithOverridePasswordAttribute("test-password"),
)),
}
libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...)
assert.NoError(err)
require.NotNil(libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(err)
require.NotNil(lib)
libs[lib.GetPublicId()] = lib
req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose}
requests = append(requests, req)
}
{
opts := []Option{
WithCredentialType(credential.UserPasswordType),
WithMappingOverride(NewUserPasswordOverride(
WithOverrideUsernameAttribute("test-username"),
WithOverridePasswordAttribute("test-password"),
)),
}
libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...)
assert.NoError(err)
require.NotNil(libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(err)
require.NotNil(lib)
libs[lib.GetPublicId()] = lib
req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.ApplicationPurpose}
requests = append(requests, req)
}
gotLibs, err := repo.getPrivateLibraries(ctx, requests)
assert.NoError(err)
@ -131,6 +198,16 @@ func TestRepository_getPrivateLibraries(t *testing.T) {
assert.Equal(want.VaultPath, got.VaultPath)
assert.Equal(want.HttpMethod, got.HttpMethod)
assert.Equal(want.HttpRequestBody, got.HttpRequestBody)
assert.Equal(want.CredentialType(), got.CredentialType())
if mo := want.MappingOverride; mo != nil {
switch w := mo.(type) {
case *UserPasswordOverride:
assert.Equal(w.UsernameAttribute, got.UsernameAttribute)
assert.Equal(w.PasswordAttribute, got.PasswordAttribute)
default:
assert.Fail("unknown mapping override type")
}
}
}
})
}
@ -227,3 +304,179 @@ func TestRequestMap(t *testing.T) {
})
}
}
func TestBaseToUsrPass(t *testing.T) {
t.Parallel()
tests := []struct {
name string
given *baseCred
want *usrPassCred
wantErr errors.Code
}{
{
name: "nil-input",
wantErr: errors.InvalidParameter,
},
{
name: "nil-library",
given: &baseCred{},
wantErr: errors.InvalidParameter,
},
{
name: "library-not-username-password-type",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UnspecifiedType),
},
},
wantErr: errors.InvalidParameter,
},
{
name: "invalid-no-username-default-password-attribute",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UserPasswordType),
},
secretData: map[string]interface{}{
"password": "my-password",
},
},
wantErr: errors.VaultInvalidCredentialMapping,
},
{
name: "invalid-no-password-default-username-attribute",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UserPasswordType),
},
secretData: map[string]interface{}{
"username": "my-username",
},
},
wantErr: errors.VaultInvalidCredentialMapping,
},
{
name: "valid-default-attributes",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UserPasswordType),
},
secretData: map[string]interface{}{
"username": "my-username",
"password": "my-password",
},
},
want: &usrPassCred{
username: "my-username",
password: credential.Password("my-password"),
},
},
{
name: "valid-override-attributes",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UserPasswordType),
UsernameAttribute: "test-username",
PasswordAttribute: "test-password",
},
secretData: map[string]interface{}{
"username": "default-username",
"password": "default-password",
"test-username": "override-username",
"test-password": "override-password",
},
},
want: &usrPassCred{
username: "override-username",
password: credential.Password("override-password"),
},
},
{
name: "valid-default-username-override-password",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UserPasswordType),
PasswordAttribute: "test-password",
},
secretData: map[string]interface{}{
"username": "default-username",
"password": "default-password",
"test-username": "override-username",
"test-password": "override-password",
},
},
want: &usrPassCred{
username: "default-username",
password: credential.Password("override-password"),
},
},
{
name: "valid-override-username-default-password",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UserPasswordType),
UsernameAttribute: "test-username",
},
secretData: map[string]interface{}{
"username": "default-username",
"password": "default-password",
"test-username": "override-username",
"test-password": "override-password",
},
},
want: &usrPassCred{
username: "override-username",
password: credential.Password("default-password"),
},
},
{
name: "invalid-username-override",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UserPasswordType),
UsernameAttribute: "missing-username",
},
secretData: map[string]interface{}{
"username": "default-username",
"password": "default-password",
"test-username": "override-username",
"test-password": "override-password",
},
},
wantErr: errors.VaultInvalidCredentialMapping,
},
{
name: "invalid-password-override",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.UserPasswordType),
UsernameAttribute: "missing-password",
},
secretData: map[string]interface{}{
"username": "default-username",
"password": "default-password",
"test-username": "override-username",
"test-password": "override-password",
},
},
wantErr: errors.VaultInvalidCredentialMapping,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := baseToUsrPass(context.Background(), tt.given)
if tt.wantErr != 0 {
assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err)
assert.Nil(got)
return
}
require.NoError(err)
want := tt.want
want.baseCred = tt.given
assert.Equal(want, got)
})
}
}

@ -2,13 +2,11 @@ package vault
import (
"context"
"fmt"
"time"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
vault "github.com/hashicorp/vault/api"
)
var _ credential.Issuer = (*Repository)(nil)
@ -36,48 +34,13 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre
var creds []credential.Dynamic
var minLease time.Duration
for _, lib := range libs {
// Get the credential ID early. No need to get a secret from Vault
// if there is no way to save it in the database.
credId, err := newCredentialId()
cred, err := lib.retrieveCredential(ctx, op, sessionId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
return nil, err
}
client, err := lib.client()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
if minLease > cred.getExpiration() {
minLease = cred.getExpiration()
}
var secret *vault.Secret
switch Method(lib.HttpMethod) {
case MethodGet:
secret, err = client.get(lib.VaultPath)
case MethodPost:
secret, err = client.post(lib.VaultPath, lib.HttpRequestBody)
default:
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unknown http method: library: %s", lib.PublicId))
}
if err != nil {
// TODO(mgaffney) 05/2021: detect if the error is because of an
// expired or invalid token
return nil, errors.Wrap(ctx, err, op)
}
if secret == nil {
return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op))
}
leaseDuration := time.Duration(secret.LeaseDuration) * time.Second
if minLease > leaseDuration {
minLease = leaseDuration
}
cred, err := newCredential(lib.GetPublicId(), sessionId, secret.LeaseID, lib.TokenHmac, leaseDuration)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
cred.PublicId = credId
cred.IsRenewable = secret.Renewable
insertQuery, insertQueryValues := cred.insertQuery()
updateQuery, updateQueryValues := cred.updateSessionQuery(lib.Purpose)
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
@ -105,13 +68,7 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre
return nil, errors.Wrap(ctx, err, op)
}
creds = append(creds, &actualCredential{
id: cred.PublicId,
sessionId: cred.SessionId,
lib: lib,
secretData: secret.Data,
purpose: lib.Purpose,
})
creds = append(creds, cred)
}
// Best effort update next run time of credential renewal job, but an error should not

@ -64,6 +64,8 @@ func TestRepository_IssueCredentials(t *testing.T) {
type libT int
const (
libDB libT = iota
libUsrPassDB
libErrUsrPassDB
libPKI
libErrPKI
libKV
@ -105,23 +107,53 @@ func TestRepository_IssueCredentials(t *testing.T) {
{
libPath := path.Join("secret", "data", "my-secret")
libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...)
assert.NoError(err)
require.NotNil(libIn)
assert.NoError(t, err)
require.NotNil(t, libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(err)
require.NotNil(lib)
assert.NoError(t, err)
require.NotNil(t, lib)
libs[libKV] = lib.GetPublicId()
}
{
libPath := path.Join("secret", "data", "fake-secret")
libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...)
assert.NoError(err)
require.NotNil(libIn)
assert.NoError(t, err)
require.NotNil(t, libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(err)
require.NotNil(lib)
assert.NoError(t, err)
require.NotNil(t, lib)
libs[libErrKV] = lib.GetPublicId()
}
{
libPath := path.Join("database", "creds", "opened")
opts := []vault.Option{
vault.WithCredentialType(credential.UserPasswordType),
}
libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...)
assert.NoError(t, err)
require.NotNil(t, libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(t, err)
require.NotNil(t, lib)
libs[libUsrPassDB] = lib.GetPublicId()
}
{
libPath := path.Join("database", "creds", "opened")
opts := []vault.Option{
vault.WithCredentialType(credential.UserPasswordType),
vault.WithMappingOverride(vault.NewUserPasswordOverride(
vault.WithOverrideUsernameAttribute("test-username"),
vault.WithOverridePasswordAttribute("test-password"),
)),
}
libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, opts...)
assert.NoError(t, err)
require.NotNil(t, libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(t, err)
require.NotNil(t, lib)
libs[libErrUsrPassDB] = lib.GetPublicId()
}
at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId())
uId := at.GetIamUserId()
@ -212,6 +244,16 @@ func TestRepository_IssueCredentials(t *testing.T) {
},
},
},
{
name: "one-valid-username-password-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libUsrPassDB],
Purpose: credential.ApplicationPurpose,
},
},
},
{
name: "invalid-kv-does-not-exist",
convertFn: rc2dc,
@ -223,6 +265,17 @@ func TestRepository_IssueCredentials(t *testing.T) {
},
wantErr: errors.VaultEmptySecret,
},
{
name: "one-valid-username-password-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libErrUsrPassDB],
Purpose: credential.ApplicationPurpose,
},
},
wantErr: errors.VaultInvalidCredentialMapping,
},
}
for _, tt := range tests {
tt := tt
@ -245,7 +298,23 @@ func TestRepository_IssueCredentials(t *testing.T) {
return
}
assert.Len(got, len(tt.requests))
assert.NoError(err)
require.NoError(err)
assert.NotZero(len(got))
for _, dc := range got {
switch dc.Library().CredentialType() {
case credential.UserPasswordType:
if upc, ok := dc.(credential.UserPassword); ok {
assert.NotEmpty(upc.Username())
assert.NotEmpty(upc.Password())
break
}
assert.Fail("want UserPassword credential from library with credential type UserPassword")
case credential.UnspecifiedType:
if _, ok := dc.(credential.UserPassword); ok {
assert.Fail("do not want UserPassword credential from library with credential type Unspecified")
}
}
}
})
}
}

@ -0,0 +1,48 @@
begin;
drop view credential_vault_library_private;
create view credential_vault_library_private as
with
password_override (library_id, username_attribute, password_attribute) as (
select library_id,
nullif(username_attribute, wt_to_sentinel('no override')),
nullif(password_attribute, wt_to_sentinel('no override'))
from credential_vault_library_user_password_mapping_override
)
select library.public_id as public_id,
library.store_id as store_id,
library.name as name,
library.description as description,
library.create_time as create_time,
library.update_time as update_time,
library.version as version,
library.vault_path as vault_path,
library.http_method as http_method,
library.http_request_body as http_request_body,
library.credential_type as credential_type,
store.scope_id as scope_id,
store.vault_address as vault_address,
store.namespace as namespace,
store.ca_cert as ca_cert,
store.tls_server_name as tls_server_name,
store.tls_skip_verify as tls_skip_verify,
store.token_hmac as token_hmac,
store.ct_token as ct_token, -- encrypted
store.token_key_id as token_key_id,
store.client_cert as client_cert,
store.ct_client_key as ct_client_key, -- encrypted
store.client_key_id as client_key_id,
upasso.username_attribute as username_attribute,
upasso.password_attribute as password_attribute
from credential_vault_library library
join credential_vault_store_private store
on library.store_id = store.public_id
left join password_override upasso
on library.public_id = upasso.library_id
and store.token_status = 'current';
comment on view credential_vault_library_private is
'credential_vault_library_private is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. '
'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.';
commit;

@ -313,6 +313,11 @@ begin;
values
('p____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default');
insert into credential_vault_token
(store_id, key_id, status, token_hmac, token, last_renewal_time, expiration_time)
values
('vs_______wvs', 'kdkv___widget', 'current', 'hmac-value', 'token-value', now(), now() + interval '1 hour');
insert into credential_vault_library
(store_id, public_id, name, description, vault_path, http_method, credential_type)
values

@ -4,7 +4,7 @@
-- delete_credential_vault_library_mapping_override_subtype
begin;
select plan(10);
select plan(11);
select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials');
-- validate the setup data
@ -16,6 +16,23 @@ begin;
from credential_vault_library_mapping_override
where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7');
prepare select_private_libraries as
select public_id::text, credential_type::text, username_attribute::text, password_attribute::text
from credential_vault_library_private
where public_id in ('vl______wvl2', 'vl______wvl3', 'vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7')
order by public_id;
select results_eq(
'select_private_libraries',
$$VALUES
('vl______wvl2', 'unspecified', null, null),
('vl______wvl3', 'user_password', null, null),
('vl______wvl4', 'user_password', null, null),
('vl______wvl5', 'user_password', 'my_username', null),
('vl______wvl6', 'user_password', null, 'my_password'),
('vl______wvl7', 'user_password', 'my_username', 'my_password')$$
);
-- validate the insert triggers
select is(count(*), 0::bigint) from credential_vault_library_user_password_mapping_override where library_id = 'vl______wvl3';
select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3';

@ -109,6 +109,7 @@ const (
VaultCredentialRequest Code = 3014 // VaultCredentialRequest represents an error returned from Vault when retrieving a credential
VaultEmptySecret Code = 3015 // VaultEmptySecret represents a empty secret was returned from Vault without error
VaultInvalidMappingOverride Code = 3016 // VaultInvalidMappingOverride represents an error returned when a credential mapping is unknown or does not match a credential type
VaultInvalidCredentialMapping Code = 3017 // VaultInvalidCredentialMapping represents an error returned when a Vault secret failed to be mapped to a specific credential type
// OIDC authentication provided errors
OidcProviderCallbackError Code = 4000 // OidcProviderCallbackError represents an error that is passed by the OIDC provider to the callback endpoint

@ -292,6 +292,11 @@ func TestCode_Both_String_Info(t *testing.T) {
c: VaultInvalidMappingOverride,
want: VaultInvalidMappingOverride,
},
{
name: "VaultInvalidCredentialMapping",
c: VaultInvalidCredentialMapping,
want: VaultInvalidCredentialMapping,
},
{
name: "OidcProviderCallbackError",
c: OidcProviderCallbackError,

@ -236,6 +236,10 @@ var errorCodeInfo = map[Code]Info{
Message: "invalid credential mapping override",
Kind: Parameter,
},
VaultInvalidCredentialMapping: {
Message: "mapping vault secret to a credential type failed",
Kind: Integrity,
},
OidcProviderCallbackError: {
Message: "oidc provider callback error",
Kind: External,

Loading…
Cancel
Save