You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/credential/vault/repository_credentials_test.go

763 lines
22 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package vault_test
import (
"context"
"path"
"testing"
"github.com/hashicorp/boundary/globals"
"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/db/timestamp"
"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"
)
// since we're not using gorm tags to retrieve this, it's faster and easier to just make a new struct than import usrPassCred from the vault package
type revokableCred struct {
PublicId string
LibraryId string
SessionId string
TokenHmac []byte
ExternalId string
IsRenewable bool
Status string
LastRenewalTime *timestamp.Timestamp
ExpirationTime *timestamp.Timestamp
}
func lookupDbCred(t *testing.T, ctx context.Context, rw *db.Db, dc credential.Dynamic) *revokableCred {
rows, err := rw.Query(ctx, `
select
public_id,
library_id,
session_id,
token_hmac,
external_id,
is_renewable,
status,
last_renewal_time,
expiration_time
from credential_vault_credential
where public_id = ?;
`, []any{dc.GetPublicId()})
require.NoError(t, err)
rowCount := 0
got := revokableCred{}
for rows.Next() {
rowCount++
require.NoError(t, rows.Scan(
&got.PublicId,
&got.LibraryId,
&got.SessionId,
&got.TokenHmac,
&got.ExternalId,
&got.IsRenewable,
&got.Status,
&got.LastRenewalTime,
&got.ExpirationTime,
))
}
assert.NoError(t, rows.Err())
// Should never get more than one that matches, but can get 0
assert.LessOrEqual(t, rowCount, 1)
if rowCount == 0 {
return nil
}
return &got
}
type libT int
const (
libDB libT = iota
libUsrPassDB
libErrUsrPassDB
libPKI
libErrPKI
libKV
libErrKV
libUsrPassKV
libSshPkKV
libExpiredToken
)
type testLib struct {
PublicId string
HasLease bool
IsRenewable bool
}
type testLibMap map[libT]testLib
func (m testLibMap) GetByPublicId(id string) (testLib, bool) {
for _, v := range m {
if v.PublicId == id {
return v, true
}
}
return testLib{}, false
}
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(ctx, 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(ctx, 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 = ?",
[]any{vault.ExpiredToken, expStore.PublicId})
require.NoError(t, err)
assert.Equal(t, 1, num)
v.RevokeToken(t, expToken)
libs := make(testLibMap)
{
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] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true}
}
{
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] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: false}
}
{
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] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true}
}
{
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] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: true}
}
{
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] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: true}
}
{
libPath := path.Join("database", "creds", "opened")
opts := []vault.Option{
vault.WithCredentialType(globals.UsernamePasswordCredentialType),
}
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] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true}
}
{
libPath := path.Join("database", "creds", "opened")
opts := []vault.Option{
vault.WithCredentialType(globals.UsernamePasswordCredentialType),
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] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true}
}
{
libPath := path.Join("secret", "data", "my-up-secret")
opts := []vault.Option{
vault.WithCredentialType(globals.UsernamePasswordCredentialType),
}
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] = testLib{PublicId: lib.GetPublicId(), HasLease: false}
}
{
libPath := path.Join("secret", "data", "my-sshpk-secret")
opts := []vault.Option{
vault.WithCredentialType(globals.SshPrivateKeyCredentialType),
}
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] = testLib{PublicId: lib.GetPublicId(), HasLease: false}
}
{
libPath := path.Join("secret", "data", "my-up-secret")
opts := []vault.Option{
vault.WithCredentialType(globals.UsernamePasswordCredentialType),
}
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] = testLib{PublicId: lib.GetPublicId(), HasLease: false}
}
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].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "multiple-valid-libraries",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libDB].PublicId,
Purpose: credential.BrokeredPurpose,
},
{
SourceId: libs[libPKI].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "one-library-that-errors",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libErrPKI].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.VaultCredentialRequest,
},
{
name: "no-session-dynamic-credentials",
convertFn: rc2nil,
requests: []credential.Request{
{
SourceId: libs[libDB].PublicId,
Purpose: credential.BrokeredPurpose,
},
{
SourceId: libs[libPKI].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.InvalidDynamicCredential,
},
{
name: "valid-kv-secret",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libKV].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "one-db-valid-username-password-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libUsrPassDB].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "invalid-kv-does-not-exist",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libErrKV].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.VaultEmptySecret,
},
{
name: "invalid-username-password-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libErrUsrPassDB].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
wantErr: errors.VaultInvalidCredentialMapping,
},
{
name: "valid-kv-username-password-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libUsrPassKV].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "valid-kv-ssh-private-key-library",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libSshPkKV].PublicId,
Purpose: credential.BrokeredPurpose,
},
},
},
{
name: "invalid-expired-token",
convertFn: rc2dc,
requests: []credential.Request{
{
SourceId: libs[libExpiredToken].PublicId,
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 globals.UsernamePasswordCredentialType:
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 globals.UnspecifiedCredentialType:
if _, ok := dc.(credential.UsernamePassword); ok {
assert.Fail("do not want UsernamePassword credential from library with credential type Unspecified")
}
}
if lib, ok := libs.GetByPublicId(dc.Library().GetPublicId()); ok {
retrieved := lookupDbCred(t, ctx, rw, dc)
if lib.HasLease {
// we also want to retrieve the cred from the db and make sure it's the same as the one returned by Issue
require.NotNil(retrieved)
require.Equal(retrieved.SessionId, sess.GetPublicId())
require.Equal(retrieved.PublicId, dc.GetPublicId())
assert.NotEmpty(retrieved.ExternalId)
assert.Equal(retrieved.IsRenewable, lib.IsRenewable)
} else {
assert.Nil(retrieved)
}
}
}
})
}
}
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
}
ctx := context.Background()
sche := scheduler.TestScheduler(t, conn, wrapper)
repo, err := vault.NewRepository(ctx, rw, rw, kms, sche)
require.NoError(err)
require.NotNil(repo)
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
}
ctx := context.Background()
sche := scheduler.TestScheduler(t, conn, wrapper)
repo, err := vault.NewRepository(ctx, rw, rw, kms, sche)
require.NoError(err)
require.NotNil(repo)
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(ctx, 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)
}