Files
threshold/internal/credential/vault/repository_credentials_test.go
T
Louis Ruch d7c4c648ec bug(vault): Correctly handle credential stores with expired tokens (#2399)
* bug(vault): Correctly handle credential stores with expired tokens

Boundary makes use of a database view to perform CRUD operations on
Vault credential stores and libraries. This view did not include
credential stores with tokens that have expired in Vault, causing
errors when attempting to perform any action against them.
2022-09-06 07:50:15 -07:00

669 lines
20 KiB
Go

package vault_test
import (
"context"
"path"
"testing"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/vault"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/scheduler"
"github.com/hashicorp/boundary/internal/session"
"github.com/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/internal/target/tcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRepository_IssueCredentials(t *testing.T) {
t.Parallel()
ctx := context.Background()
v := vault.NewTestVaultServer(t, vault.WithDockerNetwork(true), vault.WithTestVaultTLS(vault.TestClientTLS))
v.MountDatabase(t)
v.MountPKI(t)
v.AddKVPolicy(t)
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
kms := kms.TestKms(t, conn, wrapper)
sche := scheduler.TestScheduler(t, conn, wrapper)
repo, err := vault.NewRepository(rw, rw, kms, sche)
require.NoError(t, err)
require.NotNil(t, repo)
err = vault.RegisterJobs(ctx, sche, rw, rw, kms)
require.NoError(t, err)
_, token := v.CreateToken(t, vault.WithPolicies([]string{"default", "boundary-controller", "database", "pki", "secret"}))
// Create valid username password KV secret
v.CreateKVSecret(t, "my-up-secret", []byte(`{"data":{"username":"user","password":"pass"}}`))
// Create valid ssh private key KV secret
v.CreateKVSecret(t, "my-sshpk-secret", []byte(`{"data":{"username":"user","private_key":"pk data"}}`))
var opts []vault.Option
opts = append(opts, vault.WithCACert(v.CaCert))
clientCert, err := vault.NewClientCertificate(v.ClientCert, v.ClientKey)
require.NoError(t, err)
opts = append(opts, vault.WithClientCert(clientCert))
credStoreIn, err := vault.NewCredentialStore(prj.GetPublicId(), v.Addr, []byte(token), opts...)
assert.NoError(t, err)
require.NotNil(t, credStoreIn)
origStore, err := repo.CreateCredentialStore(ctx, credStoreIn)
assert.NoError(t, err)
require.NotNil(t, origStore)
_, expToken := v.CreateToken(t, vault.WithPolicies([]string{"default", "boundary-controller", "database", "pki", "secret"}))
expCredStoreIn, err := vault.NewCredentialStore(prj.GetPublicId(), v.Addr, []byte(expToken), opts...)
assert.NoError(t, err)
require.NotNil(t, expCredStoreIn)
expStore, err := repo.CreateCredentialStore(ctx, expCredStoreIn)
assert.NoError(t, err)
require.NotNil(t, expStore)
// Set previous token to expired in the database and revoke in Vault to validate a
// credential store with an expired token is correctly returned over the API
num, err := rw.Exec(context.Background(), "update credential_vault_token set status = ? where store_id = ?",
[]interface{}{vault.ExpiredToken, expStore.PublicId})
require.NoError(t, err)
assert.Equal(t, 1, num)
v.RevokeToken(t, expToken)
type libT int
const (
libDB libT = iota
libUsrPassDB
libErrUsrPassDB
libPKI
libErrPKI
libKV
libErrKV
libUsrPassKV
libSshPkKV
libExpiredToken
)
libs := make(map[libT]string)
{
libPath := path.Join("database", "creds", "opened")
libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath)
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[libDB] = lib.GetPublicId()
}
{
libPath := path.Join("pki", "issue", "boundary")
libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, vault.WithMethod(vault.MethodPost), vault.WithRequestBody([]byte(`{"common_name":"boundary.com"}`)))
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[libPKI] = lib.GetPublicId()
}
{
libPath := path.Join("pki", "issue", "boundary")
libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath, vault.WithMethod(vault.MethodPost))
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[libErrPKI] = lib.GetPublicId()
}
{
libPath := path.Join("secret", "data", "my-up-secret")
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[libKV] = lib.GetPublicId()
}
{
libPath := path.Join("secret", "data", "fake-secret")
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[libErrKV] = lib.GetPublicId()
}
{
libPath := path.Join("database", "creds", "opened")
opts := []vault.Option{
vault.WithCredentialType(credential.UsernamePasswordType),
}
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.UsernamePasswordType),
vault.WithMappingOverride(vault.NewUsernamePasswordOverride(
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()
}
{
libPath := path.Join("secret", "data", "my-up-secret")
opts := []vault.Option{
vault.WithCredentialType(credential.UsernamePasswordType),
}
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[libUsrPassKV] = lib.GetPublicId()
}
{
libPath := path.Join("secret", "data", "my-sshpk-secret")
opts := []vault.Option{
vault.WithCredentialType(credential.SshPrivateKeyType),
}
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[libSshPkKV] = lib.GetPublicId()
}
{
libPath := path.Join("secret", "data", "my-up-secret")
opts := []vault.Option{
vault.WithCredentialType(credential.UsernamePasswordType),
}
libIn, err := vault.NewCredentialLibrary(expStore.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[libExpiredToken] = lib.GetPublicId()
}
at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId())
uId := at.GetIamUserId()
hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0]
hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0]
h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0]
static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h})
tar := tcp.TestTarget(context.Background(), t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()}))
rc2dc := func(rcs []credential.Request) []*session.DynamicCredential {
var dcs []*session.DynamicCredential
for _, rc := range rcs {
dc := &session.DynamicCredential{
LibraryId: rc.SourceId,
CredentialPurpose: string(rc.Purpose),
}
dcs = append(dcs, dc)
}
return dcs
}
rc2nil := func(rcs []credential.Request) []*session.DynamicCredential { return nil }
tests := []struct {
name string
convertFn func(rcs []credential.Request) []*session.DynamicCredential
requests []credential.Request
wantErr errors.Code
}{
{
name: "one-library-valid",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libDB],
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "multiple-valid-libraries",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libDB],
Purpose: credential.BrokeredPurpose,
},
{
SourceId: libs[libPKI],
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "one-library-that-errors",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libErrPKI],
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.VaultCredentialRequest,
},
{
name: "no-session-dynamic-credentials",
convertFn: rc2nil,
requests: []credential.Request{
{
SourceId: libs[libDB],
Purpose: credential.BrokeredPurpose,
},
{
SourceId: libs[libPKI],
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.InvalidDynamicCredential,
},
{
name: "valid-kv-secret",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libKV],
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "one-db-valid-username-password-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libUsrPassDB],
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "invalid-kv-does-not-exist",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libErrKV],
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.VaultEmptySecret,
},
{
name: "invalid-username-password-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libErrUsrPassDB],
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.VaultInvalidCredentialMapping,
},
{
name: "valid-kv-username-password-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libUsrPassKV],
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "valid-kv-ssh-private-key-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libSshPkKV],
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "invalid-expired-token",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libExpiredToken],
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.InvalidParameter,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
sess := session.TestSession(t, conn, wrapper, session.ComposedOf{
UserId: uId,
HostId: h.GetPublicId(),
TargetId: tar.GetPublicId(),
HostSetId: hs.GetPublicId(),
AuthTokenId: at.GetPublicId(),
ProjectId: prj.GetPublicId(),
Endpoint: "tcp://127.0.0.1:22",
DynamicCredentials: tt.convertFn(tt.requests),
})
got, err := repo.Issue(ctx, sess.GetPublicId(), tt.requests)
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
}
assert.Len(got, len(tt.requests))
require.NoError(err)
assert.NotZero(len(got))
for _, dc := range got {
switch dc.Library().CredentialType() {
case credential.UsernamePasswordType:
if upc, ok := dc.(credential.UsernamePassword); ok {
assert.NotEmpty(upc.Username())
assert.NotEmpty(upc.Password())
break
}
assert.Fail("want UsernamePassword credential from library with credential type UsernamePassword")
case credential.UnspecifiedType:
if _, ok := dc.(credential.UsernamePassword); ok {
assert.Fail("do not want UsernamePassword credential from library with credential type Unspecified")
}
}
}
})
}
}
func TestRepository_Revoke(t *testing.T) {
t.Parallel()
assert, require := assert.New(t), require.New(t)
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
require.NotNil(prj)
assert.NotEmpty(prj.GetPublicId())
cs := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0]
cl := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 1)[0]
hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0]
hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0]
h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0]
static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h})
tar := tcp.TestTarget(context.Background(), t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()}))
target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId())
const (
sessionCount = 4
credentialCount = 4
)
sessionIds := make([]string, sessionCount)
sessions := make(map[string][]*vault.Credential, sessionCount)
for i := 0; i < sessionCount; i++ {
at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId())
uId := at.GetIamUserId()
sess := session.TestSession(t, conn, wrapper, session.ComposedOf{
UserId: uId,
HostId: h.GetPublicId(),
TargetId: tar.GetPublicId(),
HostSetId: hs.GetPublicId(),
AuthTokenId: at.GetPublicId(),
ProjectId: prj.GetPublicId(),
Endpoint: "tcp://127.0.0.1:22",
})
sessionIds[i] = sess.GetPublicId()
credentials := vault.TestCredentials(t, conn, wrapper, cl.GetPublicId(), sess.GetPublicId(), credentialCount)
assert.Len(credentials, credentialCount)
for _, credential := range credentials {
assert.NotEmpty(credential.GetPublicId())
}
sessions[sess.GetPublicId()] = credentials
}
sche := scheduler.TestScheduler(t, conn, wrapper)
repo, err := vault.NewRepository(rw, rw, kms, sche)
require.NoError(err)
require.NotNil(repo)
ctx := context.Background()
assert.Error(repo.Revoke(ctx, ""))
type credCount struct {
active, revoke int
revoked, expired, unknown int
}
assertCreds := func(want map[string]*credCount) {
const query = `
select session_id, status, count(public_id)
from credential_vault_credential
group by session_id, status;
`
got := make(map[string]*credCount, len(want))
for id := range want {
got[id] = new(credCount)
}
var (
id string
status string
count int
)
rows, err := rw.Query(ctx, query, nil)
require.NoError(err)
defer rows.Close()
for rows.Next() {
require.NoError(rows.Scan(&id, &status, &count))
switch status {
case "active":
got[id].active = count
case "revoke":
got[id].revoke = count
case "revoked":
got[id].revoked = count
case "expired":
got[id].expired = count
case "unknown":
got[id].unknown = count
default:
assert.Failf("Unexpected status: %s", status)
}
}
require.NoError(rows.Err())
assert.Equal(want, got)
}
cc := make(map[string]*credCount, sessionCount)
for _, id := range sessionIds {
cc[id] = new(credCount)
cc[id].active = credentialCount
}
for _, id := range sessionIds {
assert.NoError(repo.Revoke(ctx, id))
cc[id].revoke = cc[id].active
cc[id].active = 0
assertCreds(cc)
}
}
func Test_TerminateSession(t *testing.T) {
t.Parallel()
assert, require := assert.New(t), require.New(t)
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
require.NotNil(prj)
assert.NotEmpty(prj.GetPublicId())
cs := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0]
cl := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 1)[0]
hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0]
hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0]
h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0]
static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h})
tar := tcp.TestTarget(context.Background(), t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()}))
target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId())
const (
sessionCount = 4
credentialCount = 4
)
sessionIds := make([]string, sessionCount)
sessions := make(map[string][]*vault.Credential, sessionCount)
for i := 0; i < sessionCount; i++ {
at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId())
uId := at.GetIamUserId()
sess := session.TestSession(t, conn, wrapper, session.ComposedOf{
UserId: uId,
HostId: h.GetPublicId(),
TargetId: tar.GetPublicId(),
HostSetId: hs.GetPublicId(),
AuthTokenId: at.GetPublicId(),
ProjectId: prj.GetPublicId(),
Endpoint: "tcp://127.0.0.1:22",
})
sessionIds[i] = sess.GetPublicId()
credentials := vault.TestCredentials(t, conn, wrapper, cl.GetPublicId(), sess.GetPublicId(), credentialCount)
assert.Len(credentials, credentialCount)
for _, credential := range credentials {
assert.NotEmpty(credential.GetPublicId())
}
sessions[sess.GetPublicId()] = credentials
}
sche := scheduler.TestScheduler(t, conn, wrapper)
repo, err := vault.NewRepository(rw, rw, kms, sche)
require.NoError(err)
require.NotNil(repo)
ctx := context.Background()
assert.Error(repo.Revoke(ctx, ""))
type credCount struct {
active, revoke int
revoked, expired, unknown int
}
assertCreds := func(want map[string]*credCount) {
const query = `
select session_id, status, count(public_id)
from credential_vault_credential
group by session_id, status;
`
got := make(map[string]*credCount, len(want))
for id := range want {
got[id] = new(credCount)
}
var (
id string
status string
count int
)
rows, err := rw.Query(ctx, query, nil)
require.NoError(err)
defer rows.Close()
for rows.Next() {
require.NoError(rows.Scan(&id, &status, &count))
switch status {
case "active":
got[id].active = count
case "revoke":
got[id].revoke = count
case "revoked":
got[id].revoked = count
case "expired":
got[id].expired = count
case "unknown":
got[id].unknown = count
default:
assert.Failf("Unexpected status: %s", status)
}
}
require.NoError(rows.Err())
assert.Equal(want, got)
}
cc := make(map[string]*credCount, sessionCount)
for _, id := range sessionIds {
cc[id] = new(credCount)
cc[id].active = credentialCount
}
sessionRepo, err := session.NewRepository(rw, rw, kms)
require.NoError(err)
count, err := sessionRepo.TerminateCompletedSessions(ctx)
assert.NoError(err)
assert.Zero(count)
assertCreds(cc)
// call CancelSession
id := sessionIds[0]
_, err = sessionRepo.CancelSession(ctx, id, 1)
assert.NoError(err)
cc[id].revoke = cc[id].active
cc[id].active = 0
assertCreds(cc)
count, err = sessionRepo.TerminateCompletedSessions(ctx)
assert.NoError(err)
assert.Equal(1, count)
assertCreds(cc)
count, err = sessionRepo.TerminateCompletedSessions(ctx)
assert.NoError(err)
assert.Zero(count)
assertCreds(cc)
}