internal/credential/vault: add pagination logic

pull/4202/head
Johan Brandhorst-Satzkorn 2 years ago
parent fcb47f3772
commit 1337a57674

@ -1,14 +1,16 @@
// Copyright (c) HashiCorp, Inc.
// Copyright (c) HashiCorp Inc.
// SPDX-License-Identifier: BUSL-1.1
package vault
import (
"context"
"fmt"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/vault/store"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/resource"
@ -132,3 +134,89 @@ func (l *CredentialLibrary) CredentialType() globals.CredentialType {
}
var _ credential.Library = (*CredentialLibrary)(nil)
// listCredentialLibraryResult represents the result of the
// list queries used to list all credential libraries.
type listCredentialLibraryResult struct {
PublicId string
StoreId string
ProjectId string
Name string
Description string
VaultPath string
CredentialType string
HttpMethod string
HttpRequestBody string
Username string
KeyType string
Ttl string
KeyId string
CriticalOptions string
Extensions string
AdditionalValidPrincipals string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version int
KeyBits int
Type string
}
func (l *listCredentialLibraryResult) toLibrary(ctx context.Context) (credential.Library, error) {
const op = "vault.(*listCredentialLibraryResult).toLibrary"
switch l.Type {
case "generic":
cl := &CredentialLibrary{
CredentialLibrary: &store.CredentialLibrary{
PublicId: l.PublicId,
StoreId: l.StoreId,
Name: l.Name,
Description: l.Description,
CreateTime: l.CreateTime,
UpdateTime: l.UpdateTime,
Version: uint32(l.Version),
VaultPath: l.VaultPath,
CredentialType: l.CredentialType,
HttpMethod: l.HttpMethod,
},
}
// Assign byte slices only if the string isn't empty
if l.HttpRequestBody != "" {
cl.HttpRequestBody = []byte(l.HttpRequestBody)
}
return cl, nil
case "ssh":
return &SSHCertificateCredentialLibrary{
SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{
PublicId: l.PublicId,
StoreId: l.StoreId,
Name: l.Name,
Description: l.Description,
CreateTime: l.CreateTime,
UpdateTime: l.UpdateTime,
Version: uint32(l.Version),
VaultPath: l.VaultPath,
CredentialType: l.CredentialType,
Username: l.Username,
KeyType: l.KeyType,
KeyBits: uint32(l.KeyBits),
Ttl: l.Ttl,
KeyId: l.KeyId,
CriticalOptions: l.CriticalOptions,
Extensions: l.Extensions,
AdditionalValidPrincipals: l.AdditionalValidPrincipals,
},
}, nil
default:
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unexpected vault credential library type %s returned", l.Type))
}
}
type deletedCredentialLibrary struct {
PublicId string `gorm:"primary_key"`
DeleteTime *timestamp.Timestamp
}
// TableName returns the tablename to override the default gorm table name
func (s *deletedCredentialLibrary) TableName() string {
return "credential_vault_library_deleted"
}

@ -188,5 +188,298 @@ delete_time is not null
delete from credential_vault_credential
where session_id is null
and status not in ('active', 'revoke')
`
estimateCountCredentialLibraries = `
select sum(reltuples::bigint) as estimate
from pg_class
where oid in (
'credential_vault_library'::regclass,
'credential_vault_ssh_cert_library'::regclass
)
`
listLibrariesTemplate = `
with libraries as (
select public_id
from credential_library
where store_id = @store_id
order by create_time desc, public_id asc
limit %d
),
generic_libs as (
select *
from credential_vault_library
where public_id in (select public_id from libraries)
),
ssh_cert_libs as (
select *
from credential_vault_ssh_cert_library
where public_id in (select public_id from libraries)
),
final as (
select public_id,
store_id,
project_id,
name,
description,
create_time,
update_time,
version,
vault_path,
credential_type,
http_method,
http_request_body,
null as username, -- Add to make union uniform
null as key_type, -- Add to make union uniform
null as key_bits, -- Add to make union uniform
null as ttl, -- Add to make union uniform
null as key_id, -- Add to make union uniform
null as critical_options, -- Add to make union uniform
null as extensions, -- Add to make union uniform
null as additional_valid_principals, -- Add to make union uniform
'generic' as type
from generic_libs
union
select public_id,
store_id,
project_id,
name,
description,
create_time,
update_time,
version,
vault_path,
credential_type,
null as http_method, -- Add to make union uniform
null as http_request_body, -- Add to make union uniform
username,
key_type,
key_bits,
ttl,
key_id,
critical_options,
extensions,
additional_valid_principals,
'ssh' as type
from ssh_cert_libs
)
select *
from final
order by create_time desc, public_id asc;
`
listLibrariesPageTemplate = `
with libraries as (
select public_id
from credential_library
where store_id = @store_id
and (create_time, public_id) < (@last_item_create_time, @last_item_id)
order by create_time desc, public_id asc
limit %d
),
generic_libs as (
select *
from credential_vault_library
where public_id in (select public_id from libraries)
),
ssh_cert_libs as (
select *
from credential_vault_ssh_cert_library
where public_id in (select public_id from libraries)
),
final as (
select public_id,
store_id,
project_id,
name,
description,
create_time,
update_time,
version,
vault_path,
credential_type,
http_method,
http_request_body,
null as username, -- Add to make union uniform
null as key_type, -- Add to make union uniform
null as key_bits, -- Add to make union uniform
null as ttl, -- Add to make union uniform
null as key_id, -- Add to make union uniform
null as critical_options, -- Add to make union uniform
null as extensions, -- Add to make union uniform
null as additional_valid_principals, -- Add to make union uniform
'generic' as type
from generic_libs
union
select public_id,
store_id,
project_id,
name,
description,
create_time,
update_time,
version,
vault_path,
credential_type,
null as http_method, -- Add to make union uniform
null as http_request_body, -- Add to make union uniform
username,
key_type,
key_bits,
ttl,
key_id,
critical_options,
extensions,
additional_valid_principals,
'ssh' as type
from ssh_cert_libs
)
select *
from final
order by create_time desc, public_id asc;
`
listLibrariesRefreshTemplate = `
with libraries as (
select public_id
from credential_library
where store_id = @store_id
and update_time > @updated_after_time
order by update_time desc, public_id asc
limit %d
),
generic_libs as (
select *
from credential_vault_library
where public_id in (select public_id from libraries)
),
ssh_cert_libs as (
select *
from credential_vault_ssh_cert_library
where public_id in (select public_id from libraries)
),
final as (
select public_id,
store_id,
project_id,
name,
description,
create_time,
update_time,
version,
vault_path,
credential_type,
http_method,
http_request_body,
null as username, -- Add to make union uniform
null as key_type, -- Add to make union uniform
null as key_bits, -- Add to make union uniform
null as ttl, -- Add to make union uniform
null as key_id, -- Add to make union uniform
null as critical_options, -- Add to make union uniform
null as extensions, -- Add to make union uniform
null as additional_valid_principals, -- Add to make union uniform
'generic' as type
from generic_libs
union
select public_id,
store_id,
project_id,
name,
description,
create_time,
update_time,
version,
vault_path,
credential_type,
null as http_method, -- Add to make union uniform
null as http_request_body, -- Add to make union uniform
username,
key_type,
key_bits,
ttl,
key_id,
critical_options,
extensions,
additional_valid_principals,
'ssh' as type
from ssh_cert_libs
)
select *
from final
order by update_time desc, public_id asc;
`
listLibrariesRefreshPageTemplate = `
with libraries as (
select public_id
from credential_library
where store_id = @store_id
and update_time > @updated_after_time
and (update_time, public_id) < (@last_item_update_time, @last_item_id)
order by update_time desc, public_id asc
limit %d
),
generic_libs as (
select *
from credential_vault_library
where public_id in (select public_id from libraries)
),
ssh_cert_libs as (
select *
from credential_vault_ssh_cert_library
where public_id in (select public_id from libraries)
),
final as (
select public_id,
store_id,
project_id,
name,
description,
create_time,
update_time,
version,
vault_path,
credential_type,
http_method,
http_request_body,
null as username, -- Add to make union uniform
null as key_type, -- Add to make union uniform
null as key_bits, -- Add to make union uniform
null as ttl, -- Add to make union uniform
null as key_id, -- Add to make union uniform
null as critical_options, -- Add to make union uniform
null as extensions, -- Add to make union uniform
null as additional_valid_principals, -- Add to make union uniform
'generic' as type
from generic_libs
union
select public_id,
store_id,
project_id,
name,
description,
create_time,
update_time,
version,
vault_path,
credential_type,
null as http_method, -- Add to make union uniform
null as http_request_body, -- Add to make union uniform
username,
key_type,
key_bits,
ttl,
key_id,
critical_options,
extensions,
additional_valid_principals,
'ssh' as type
from ssh_cert_libs
)
select *
from final
order by update_time desc, public_id asc;
`
)

@ -5,10 +5,14 @@ package vault
import (
"context"
"database/sql"
"fmt"
"slices"
"strings"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
@ -452,23 +456,173 @@ func (r *Repository) DeleteCredentialLibrary(ctx context.Context, projectId stri
return rowsDeleted, nil
}
// ListCredentialLibraries returns a slice of CredentialLibraries for the
// storeId. WithLimit is the only option supported.
func (r *Repository) ListCredentialLibraries(ctx context.Context, storeId string, opt ...Option) ([]*CredentialLibrary, error) {
const op = "vault.(Repository).ListCredentialLibraries"
// ListLibraries returns a slice of CredentialLibraries for the
// storeId. Supports the following options:
// - credential.WithLimit
// - credential.WithStartPageAfterItem
func (r *Repository) ListLibraries(ctx context.Context, storeId string, opt ...credential.Option) ([]credential.Library, time.Time, error) {
const op = "vault.(Repository).ListLibraries"
if storeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no storeId")
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing store id")
}
opts, err := credential.GetOpts(opt...)
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
opts := getOpts(opt...)
limit := r.defaultLimit
if opts.withLimit != 0 {
if opts.WithLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
limit = opts.WithLimit
}
var libs []*CredentialLibrary
err := r.reader.SearchWhere(ctx, &libs, "store_id = ?", []any{storeId}, db.WithLimit(limit))
query := fmt.Sprintf(listLibrariesTemplate, limit)
args := []any{sql.Named("store_id", storeId)}
if opts.WithStartPageAfterItem != nil {
query = fmt.Sprintf(listLibrariesPageTemplate, limit)
args = append(args,
sql.Named("last_item_create_time", opts.WithStartPageAfterItem.GetCreateTime()),
sql.Named("last_item_id", opts.WithStartPageAfterItem.GetPublicId()),
)
}
libs, transactionTimestamp, err := r.queryLibraries(ctx, query, args)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
// Sort final slice to ensure correct ordering.
// We sort by create time descending (most recently created first).
slices.SortFunc(libs, func(i, j credential.Library) int {
return j.GetCreateTime().AsTime().Compare(i.GetCreateTime().AsTime())
})
return libs, transactionTimestamp, nil
}
// ListLibrariesRefresh returns a slice of credential libraries
// for the store ID. Supports the following options:
// - credential.WithLimit
// - credential.WithStartPageAfterItem
func (r *Repository) ListLibrariesRefresh(ctx context.Context, storeId string, updatedAfter time.Time, opt ...credential.Option) ([]credential.Library, time.Time, error) {
const op = "vault.(Repository).ListLibrariesRefresh"
switch {
case storeId == "":
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing credential store ID")
case updatedAfter.IsZero():
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updated after time")
}
opts, err := credential.GetOpts(opt...)
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
limit := r.defaultLimit
if opts.WithLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.WithLimit
}
query := fmt.Sprintf(listLibrariesRefreshTemplate, limit)
args := []any{
sql.Named("store_id", storeId),
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
}
if opts.WithStartPageAfterItem != nil {
query = fmt.Sprintf(listLibrariesRefreshPageTemplate, limit)
args = append(args,
sql.Named("last_item_update_time", opts.WithStartPageAfterItem.GetUpdateTime()),
sql.Named("last_item_id", opts.WithStartPageAfterItem.GetPublicId()),
)
}
libs, transactionTimestamp, err := r.queryLibraries(ctx, query, args)
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
// Sort final slice to ensure correct ordering.
// We sort by update time descending (most recently updated first).
slices.SortFunc(libs, func(i, j credential.Library) int {
return j.GetUpdateTime().AsTime().Compare(i.GetUpdateTime().AsTime())
})
return libs, transactionTimestamp, nil
}
func (r *Repository) queryLibraries(ctx context.Context, query string, args []any) ([]credential.Library, time.Time, error) {
const op = "vault.(Repository).queryLibraries"
var libs []credential.Library
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error {
rows, err := rd.Query(ctx, query, args)
if err != nil {
return errors.Wrap(ctx, err, op)
}
var results []listCredentialLibraryResult
for rows.Next() {
if err := rd.ScanRows(ctx, rows, &results); err != nil {
return errors.Wrap(ctx, err, op)
}
}
for _, result := range results {
lib, err := result.toLibrary(ctx)
if err != nil {
return errors.Wrap(ctx, err, op)
}
libs = append(libs, lib)
}
transactionTimestamp, err = rd.Now(ctx)
return err
}); err != nil {
return nil, time.Time{}, err
}
return libs, transactionTimestamp, nil
}
// EstimatedLibraryCount returns an estimate of the number of Vault credential libraries
func (r *Repository) EstimatedLibraryCount(ctx context.Context) (int, error) {
const op = "vault.(Repository).EstimatedLibraryCount"
rows, err := r.reader.Query(ctx, estimateCountCredentialLibraries, nil)
if err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total vault credential libraries"))
}
var count int
for rows.Next() {
if err := r.reader.ScanRows(ctx, rows, &count); err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total vault credential libraries"))
}
}
return count, nil
}
// ListDeletedLibraryIds lists the public IDs of any credential libraries deleted since the timestamp provided.
func (r *Repository) ListDeletedLibraryIds(ctx context.Context, since time.Time) ([]string, time.Time, error) {
const op = "vault.(Repository).ListDeletedLibraryIds"
var credentialLibraryIds []string
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
var deletedCredentialLibraries []*deletedCredentialLibrary
if err := r.SearchWhere(ctx, &deletedCredentialLibraries, "delete_time >= ?", []any{since}); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted credential libraries"))
}
for _, cl := range deletedCredentialLibraries {
credentialLibraryIds = append(credentialLibraryIds, cl.PublicId)
}
var deletedSshCredentialLibraries []*deletedSSHCertificateCredentialLibrary
if err := r.SearchWhere(ctx, &deletedSshCredentialLibraries, "delete_time >= ?", []any{since}); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted SSH certificate credential libraries"))
}
for _, cl := range deletedSshCredentialLibraries {
credentialLibraryIds = append(credentialLibraryIds, cl.PublicId)
}
var err error
transactionTimestamp, err = r.Now(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query transaction timestamp"))
}
return nil
}); err != nil {
return nil, time.Time{}, err
}
return libs, nil
return credentialLibraryIds, transactionTimestamp, nil
}

@ -8,10 +8,14 @@ import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/vault/store"
"github.com/hashicorp/boundary/internal/db"
dbassert "github.com/hashicorp/boundary/internal/db/assert"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
@ -19,6 +23,7 @@ import (
"github.com/hashicorp/boundary/internal/scheduler"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestRepository_CreateCredentialLibrary(t *testing.T) {
@ -2079,17 +2084,26 @@ func TestRepository_ListCredentialLibraries(t *testing.T) {
require.NotNil(orig)
// test
got, err := repo.ListCredentialLibraries(ctx, cs.GetPublicId())
got, ttime, err := repo.ListLibraries(ctx, cs.GetPublicId())
assert.NoError(err)
require.Len(got, 1)
got1 := got[0]
assert.Equal(orig.GetPublicId(), got1.GetPublicId())
assert.Equal(orig.GetStoreId(), got1.GetStoreId())
assert.Equal(orig.GetHttpMethod(), got1.GetHttpMethod())
assert.Equal(orig.GetVaultPath(), got1.GetVaultPath())
assert.Equal(orig.GetName(), got1.GetName())
assert.Equal(orig.GetCredentialType(), got1.GetCredentialType())
assert.Empty(got1.MappingOverride)
// Transaction timestamp should be within ~10 seconds of now
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
require.Empty(cmp.Diff(
orig,
got[0],
cmpopts.IgnoreUnexported(
CredentialLibrary{},
store.CredentialLibrary{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
cmpopts.IgnoreFields(
CredentialLibrary{},
"MappingOverride",
),
))
})
t.Run("with-no-credential-store-id", func(t *testing.T) {
@ -2102,10 +2116,9 @@ func TestRepository_ListCredentialLibraries(t *testing.T) {
assert.NoError(err)
require.NotNil(repo)
// test
got, err := repo.ListCredentialLibraries(ctx, "")
_, _, err = repo.ListLibraries(ctx, "")
wantErr := errors.InvalidParameter
assert.Truef(errors.Match(errors.T(wantErr), err), "want err: %q got: %q", wantErr, err)
assert.Nil(got)
})
t.Run("CredentialStore-with-no-libraries", func(t *testing.T) {
@ -2120,9 +2133,12 @@ func TestRepository_ListCredentialLibraries(t *testing.T) {
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0]
// test
got, err := repo.ListCredentialLibraries(ctx, cs.GetPublicId())
got, ttime, err := repo.ListLibraries(ctx, cs.GetPublicId())
assert.NoError(err)
assert.Empty(got)
// Transaction timestamp should be within ~10 seconds of now
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
})
}
@ -2141,7 +2157,7 @@ func TestRepository_ListCredentialLibraries_Limits(t *testing.T) {
tests := []struct {
name string
repoOpts []Option
listOpts []Option
listOpts []credential.Option
wantLen int
}{
{
@ -2153,31 +2169,21 @@ func TestRepository_ListCredentialLibraries_Limits(t *testing.T) {
repoOpts: []Option{WithLimit(3)},
wantLen: 3,
},
{
name: "with-negative-repo-limit",
repoOpts: []Option{WithLimit(-1)},
wantLen: count,
},
{
name: "with-list-limit",
listOpts: []Option{WithLimit(3)},
listOpts: []credential.Option{credential.WithLimit(3)},
wantLen: 3,
},
{
name: "with-negative-list-limit",
listOpts: []Option{WithLimit(-1)},
wantLen: count,
},
{
name: "with-repo-smaller-than-list-limit",
repoOpts: []Option{WithLimit(2)},
listOpts: []Option{WithLimit(6)},
listOpts: []credential.Option{credential.WithLimit(6)},
wantLen: 6,
},
{
name: "with-repo-larger-than-list-limit",
repoOpts: []Option{WithLimit(6)},
listOpts: []Option{WithLimit(2)},
listOpts: []credential.Option{credential.WithLimit(2)},
wantLen: 2,
},
}
@ -2191,9 +2197,155 @@ func TestRepository_ListCredentialLibraries_Limits(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw, kms, sche, tt.repoOpts...)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListCredentialLibraries(ctx, libs[0].StoreId, tt.listOpts...)
got, ttime, err := repo.ListLibraries(ctx, libs[0].StoreId, tt.listOpts...)
require.NoError(err)
assert.Len(got, tt.wantLen)
// Transaction timestamp should be within ~10 seconds of now
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
})
}
}
func TestRepository_ListCredentialLibraries_Pagination(t *testing.T) {
t.Parallel()
assert, require := assert.New(t), require.New(t)
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
sche := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 3)
for _, cs := range css[:2] { // Leave the third store empty
TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 5)
}
repo, err := NewRepository(ctx, rw, rw, kms, sche)
require.NoError(err)
require.NotNil(repo)
for _, cs := range css[:2] {
page1, ttime, err := repo.ListLibraries(ctx, cs.GetPublicId(), credential.WithLimit(2))
require.NoError(err)
require.Len(page1, 2)
// Transaction timestamp should be within ~10 seconds of now
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
page2, ttime, err := repo.ListLibraries(ctx, cs.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page1[1]))
require.NoError(err)
require.Len(page2, 2)
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
for _, item := range page1 {
assert.NotEqual(item.GetPublicId(), page2[0].GetPublicId())
assert.NotEqual(item.GetPublicId(), page2[1].GetPublicId())
}
page3, ttime, err := repo.ListLibraries(ctx, cs.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page2[1]))
require.NoError(err)
require.Len(page3, 1)
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
for _, item := range append(page1, page2...) {
assert.NotEqual(item.GetPublicId(), page3[0].GetPublicId())
}
page4, ttime, err := repo.ListLibraries(ctx, cs.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page3[0]))
require.NoError(err)
require.Empty(page4)
}
emptyPage, ttime, err := repo.ListLibraries(ctx, css[2].GetPublicId(), credential.WithLimit(2))
require.NoError(err)
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
require.Empty(emptyPage)
}
func TestRepository_ListDeletedLibraryIds(t *testing.T) {
t.Parallel()
require := require.New(t)
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
sche := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
store := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0]
libs := TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 2)
repo, err := NewRepository(ctx, rw, rw, kms, sche)
require.NoError(err)
require.NotNil(repo)
// Expect no entries at the start
deletedIds, ttime, err := repo.ListDeletedLibraryIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(err)
require.Empty(deletedIds)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
// Delete a vault library
_, err = repo.DeleteCredentialLibrary(ctx, prj.GetPublicId(), libs[0].GetPublicId())
require.NoError(err)
// Expect a single entry
deletedIds, ttime, err = repo.ListDeletedLibraryIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(err)
require.Equal([]string{libs[0].GetPublicId()}, deletedIds)
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
// Try again with the time set to now, expect no entries
deletedIds, ttime, err = repo.ListDeletedLibraryIds(ctx, time.Now())
require.NoError(err)
require.Empty(deletedIds)
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
}
func TestRepository_EstimatedLibraryCount(t *testing.T) {
t.Parallel()
assert, require := assert.New(t), require.New(t)
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
sqlDb, err := conn.SqlDB(ctx)
require.NoError(err)
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
sche := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
store := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0]
repo, err := NewRepository(ctx, rw, rw, kms, sche)
require.NoError(err)
require.NotNil(repo)
// Check total entries at start, expect 0
numItems, err := repo.EstimatedLibraryCount(ctx)
require.NoError(err)
assert.Equal(0, numItems)
// Create some libraries
libs := TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 2)
// Run analyze to update postgres meta tables
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(err)
numItems, err = repo.EstimatedLibraryCount(ctx)
require.NoError(err)
assert.Equal(2, numItems)
// Delete a library
_, err = repo.DeleteCredentialLibrary(ctx, prj.GetPublicId(), libs[0].GetPublicId())
require.NoError(err)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(err)
numItems, err = repo.EstimatedLibraryCount(ctx)
require.NoError(err)
assert.Equal(1, numItems)
}

@ -1580,7 +1580,7 @@ group by store_id, status;
}
{
libs, err := repo.ListCredentialLibraries(ctx, storeId)
libs, _, err := repo.ListLibraries(ctx, storeId)
assert.NoError(err)
assert.Len(libs, len(actualLibs))
}
@ -1640,7 +1640,7 @@ group by store_id, status;
// libraries should be empty
{
libs, err := repo.ListCredentialLibraries(ctx, storeId)
libs, _, err := repo.ListLibraries(ctx, storeId)
assert.NoError(err)
assert.Empty(libs)
}

@ -306,27 +306,6 @@ func (r *Repository) LookupSSHCertificateCredentialLibrary(ctx context.Context,
return l, nil
}
// ListSSHCertificateCredentialLibraries returns a slice of SSHCertificateCredentialLibraries for the
// storeId. WithLimit is the only option supported.
func (r *Repository) ListSSHCertificateCredentialLibraries(ctx context.Context, storeId string, opt ...Option) ([]*SSHCertificateCredentialLibrary, error) {
const op = "vault.(Repository).ListSSHCertificateCredentialLibraries"
if storeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no storeId")
}
opts := getOpts(opt...)
limit := r.defaultLimit
if opts.withLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
}
var libs []*SSHCertificateCredentialLibrary
err := r.reader.SearchWhere(ctx, &libs, "store_id = ?", []any{storeId}, db.WithLimit(limit))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return libs, nil
}
// DeleteSSHCertificateCredentialLibrary deletes publicId from the repository and returns
// the number of records deleted.
func (r *Repository) DeleteSSHCertificateCredentialLibrary(ctx context.Context, projectId string, publicId string, _ ...Option) (int, error) {

@ -811,78 +811,6 @@ func TestRepository_LookupSSHCertificateCredentialLibrary(t *testing.T) {
})
}
func TestRepository_ListSSHCertificateCredentialLibraries_Limits(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
sche := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0]
const count = 10
libs := TestSSHCertificateCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), count)
tests := []struct {
name string
repoOpts []Option
listOpts []Option
wantLen int
}{
{
name: "with-no-limits",
wantLen: count,
},
{
name: "with-repo-limit",
repoOpts: []Option{WithLimit(3)},
wantLen: 3,
},
{
name: "with-negative-repo-limit",
repoOpts: []Option{WithLimit(-1)},
wantLen: count,
},
{
name: "with-list-limit",
listOpts: []Option{WithLimit(3)},
wantLen: 3,
},
{
name: "with-negative-list-limit",
listOpts: []Option{WithLimit(-1)},
wantLen: count,
},
{
name: "with-repo-smaller-than-list-limit",
repoOpts: []Option{WithLimit(2)},
listOpts: []Option{WithLimit(6)},
wantLen: 6,
},
{
name: "with-repo-larger-than-list-limit",
repoOpts: []Option{WithLimit(6)},
listOpts: []Option{WithLimit(2)},
wantLen: 2,
},
}
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()
kms := kms.TestKms(t, conn, wrapper)
repo, err := NewRepository(ctx, rw, rw, kms, sche, tt.repoOpts...)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListSSHCertificateCredentialLibraries(ctx, libs[0].StoreId, tt.listOpts...)
require.NoError(err)
assert.Len(got, tt.wantLen)
})
}
}
func TestRepository_UpdateSSHCertificateCredentialLibrary(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/vault/store"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/resource"
"google.golang.org/protobuf/proto"
@ -131,3 +132,13 @@ func (l *SSHCertificateCredentialLibrary) CredentialType() globals.CredentialTyp
}
var _ credential.Library = (*SSHCertificateCredentialLibrary)(nil)
type deletedSSHCertificateCredentialLibrary struct {
PublicId string `gorm:"primary_key"`
DeleteTime *timestamp.Timestamp
}
// TableName returns the tablename to override the default gorm table name
func (s *deletedSSHCertificateCredentialLibrary) TableName() string {
return "credential_vault_ssh_cert_library_deleted"
}

Loading…
Cancel
Save