internal/credential: add store pagination logic
This commit is contained in:
1 parent
3197ab763f
commit
a7ebb7a3ef
11 files changed
+1905
No files matched your search
@@ -0,0 +1,363 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential
|
||||
|
||||
const (
|
||||
estimateCountStoresQuery = `
|
||||
select sum(reltuples::bigint) as estimate from pg_class where oid in (
|
||||
'credential_vault_store'::regclass,
|
||||
'credential_static_store'::regclass
|
||||
)
|
||||
`
|
||||
|
||||
listDeletedIdsQuery = `
|
||||
select public_id
|
||||
from credential_vault_store_deleted
|
||||
where delete_time >= @since
|
||||
union
|
||||
select public_id
|
||||
from credential_static_store_deleted
|
||||
where delete_time >= @since
|
||||
`
|
||||
|
||||
listStoresTemplate = `
|
||||
with stores as (
|
||||
select public_id
|
||||
from credential_store
|
||||
where %s -- search condition for project IDs is constructed
|
||||
order by create_time desc, public_id asc
|
||||
limit %d
|
||||
),
|
||||
vault_stores as (
|
||||
select *
|
||||
from credential_vault_store
|
||||
where public_id in (select public_id from stores)
|
||||
and delete_time is null
|
||||
),
|
||||
vault_tokens as (
|
||||
select store_id,
|
||||
token_hmac,
|
||||
status
|
||||
from credential_vault_token
|
||||
where store_id in (select public_id from stores)
|
||||
and status = 'current'
|
||||
),
|
||||
vault_client_certs as (
|
||||
select store_id,
|
||||
certificate,
|
||||
certificate_key_hmac
|
||||
from credential_vault_client_certificate
|
||||
where store_id in (select public_id from stores)
|
||||
),
|
||||
static_stores as (
|
||||
select *
|
||||
from credential_static_store
|
||||
where public_id in (select public_id from stores)
|
||||
),
|
||||
final as (
|
||||
select store.public_id,
|
||||
store.project_id,
|
||||
store.name,
|
||||
store.description,
|
||||
store.create_time,
|
||||
store.update_time,
|
||||
store.version,
|
||||
store.delete_time,
|
||||
store.vault_address,
|
||||
store.namespace,
|
||||
store.ca_cert,
|
||||
store.tls_server_name,
|
||||
store.tls_skip_verify,
|
||||
store.worker_filter,
|
||||
token.token_hmac as token_hmac,
|
||||
coalesce(token.status, 'expired') as token_status,
|
||||
cert.certificate as client_cert,
|
||||
cert.certificate_key_hmac as client_cert_key_hmac,
|
||||
'vault' as subtype
|
||||
from vault_stores store
|
||||
left join vault_tokens token on store.public_id = token.store_id
|
||||
left join vault_client_certs cert on store.public_id = cert.store_id
|
||||
union
|
||||
select public_id,
|
||||
project_id,
|
||||
name,
|
||||
description,
|
||||
create_time,
|
||||
update_time,
|
||||
version,
|
||||
null as delete_time, -- Add to make union uniform
|
||||
null as vault_address, -- Add to make union uniform
|
||||
null as namespace, -- Add to make union uniform
|
||||
null as ca_cert, -- Add to make union uniform
|
||||
null as tls_server_name, -- Add to make union uniform
|
||||
null as tls_skip_verify, -- Add to make union uniform
|
||||
null as worker_filter, -- Add to make union uniform
|
||||
null as token_hmac, -- Add to make union uniform
|
||||
null as token_status, -- Add to make union uniform
|
||||
null as client_cert, -- Add to make union uniform
|
||||
null as client_cert_key_hmac, -- Add to make union uniform
|
||||
'static' as subtype
|
||||
from static_stores
|
||||
)
|
||||
select *
|
||||
from final
|
||||
order by create_time desc, public_id asc;
|
||||
`
|
||||
|
||||
listStoresPageTemplate = `
|
||||
with stores as (
|
||||
select public_id
|
||||
from credential_store
|
||||
where (create_time, public_id) < (@last_item_create_time, @last_item_id) and
|
||||
%s -- search condition for project IDs is constructed
|
||||
order by create_time desc, public_id asc
|
||||
limit %d
|
||||
),
|
||||
vault_stores as (
|
||||
select *
|
||||
from credential_vault_store
|
||||
where public_id in (select public_id from stores)
|
||||
and delete_time is null
|
||||
),
|
||||
vault_tokens as (
|
||||
select store_id,
|
||||
token_hmac,
|
||||
status
|
||||
from credential_vault_token
|
||||
where store_id in (select public_id from stores)
|
||||
and status = 'current'
|
||||
),
|
||||
vault_client_certs as (
|
||||
select store_id,
|
||||
certificate,
|
||||
certificate_key_hmac
|
||||
from credential_vault_client_certificate
|
||||
where store_id in (select public_id from stores)
|
||||
),
|
||||
static_stores as (
|
||||
select *
|
||||
from credential_static_store
|
||||
where public_id in (select public_id from stores)
|
||||
),
|
||||
final as (
|
||||
select store.public_id,
|
||||
store.project_id,
|
||||
store.name,
|
||||
store.description,
|
||||
store.create_time,
|
||||
store.update_time,
|
||||
store.version,
|
||||
store.delete_time,
|
||||
store.vault_address,
|
||||
store.namespace,
|
||||
store.ca_cert,
|
||||
store.tls_server_name,
|
||||
store.tls_skip_verify,
|
||||
store.worker_filter,
|
||||
token.token_hmac as token_hmac,
|
||||
coalesce(token.status, 'expired') as token_status,
|
||||
cert.certificate as client_cert,
|
||||
cert.certificate_key_hmac as client_cert_key_hmac,
|
||||
'vault' as subtype
|
||||
from vault_stores store
|
||||
left join vault_tokens token on store.public_id = token.store_id
|
||||
left join vault_client_certs cert on store.public_id = cert.store_id
|
||||
union
|
||||
select public_id,
|
||||
project_id,
|
||||
name,
|
||||
description,
|
||||
create_time,
|
||||
update_time,
|
||||
version,
|
||||
null as delete_time, -- Add to make union uniform
|
||||
null as vault_address, -- Add to make union uniform
|
||||
null as namespace, -- Add to make union uniform
|
||||
null as ca_cert, -- Add to make union uniform
|
||||
null as tls_server_name, -- Add to make union uniform
|
||||
null as tls_skip_verify, -- Add to make union uniform
|
||||
null as worker_filter, -- Add to make union uniform
|
||||
null as token_hmac, -- Add to make union uniform
|
||||
null as token_status, -- Add to make union uniform
|
||||
null as client_cert, -- Add to make union uniform
|
||||
null as client_cert_key_hmac, -- Add to make union uniform
|
||||
'static' as subtype
|
||||
from static_stores
|
||||
)
|
||||
select *
|
||||
from final
|
||||
order by create_time desc, public_id asc;
|
||||
`
|
||||
|
||||
listStoresRefreshTemplate = `
|
||||
with stores as (
|
||||
select public_id
|
||||
from credential_store
|
||||
where update_time > @updated_after_time and
|
||||
%s -- search condition for project IDs is constructed
|
||||
order by update_time desc, public_id asc
|
||||
limit %d
|
||||
),
|
||||
vault_stores as (
|
||||
select *
|
||||
from credential_vault_store
|
||||
where public_id in (select public_id from stores)
|
||||
and delete_time is null
|
||||
),
|
||||
vault_tokens as (
|
||||
select store_id,
|
||||
token_hmac,
|
||||
status
|
||||
from credential_vault_token
|
||||
where store_id in (select public_id from stores)
|
||||
and status = 'current'
|
||||
),
|
||||
vault_client_certs as (
|
||||
select store_id,
|
||||
certificate,
|
||||
certificate_key_hmac
|
||||
from credential_vault_client_certificate
|
||||
where store_id in (select public_id from stores)
|
||||
),
|
||||
static_stores as (
|
||||
select *
|
||||
from credential_static_store
|
||||
where public_id in (select public_id from stores)
|
||||
),
|
||||
final as (
|
||||
select store.public_id,
|
||||
store.project_id,
|
||||
store.name,
|
||||
store.description,
|
||||
store.create_time,
|
||||
store.update_time,
|
||||
store.version,
|
||||
store.delete_time,
|
||||
store.vault_address,
|
||||
store.namespace,
|
||||
store.ca_cert,
|
||||
store.tls_server_name,
|
||||
store.tls_skip_verify,
|
||||
store.worker_filter,
|
||||
token.token_hmac as token_hmac,
|
||||
coalesce(token.status, 'expired') as token_status,
|
||||
cert.certificate as client_cert,
|
||||
cert.certificate_key_hmac as client_cert_key_hmac,
|
||||
'vault' as subtype
|
||||
from vault_stores store
|
||||
left join vault_tokens token on store.public_id = token.store_id
|
||||
left join vault_client_certs cert on store.public_id = cert.store_id
|
||||
union
|
||||
select public_id,
|
||||
project_id,
|
||||
name,
|
||||
description,
|
||||
create_time,
|
||||
update_time,
|
||||
version,
|
||||
null as delete_time, -- Add to make union uniform
|
||||
null as vault_address, -- Add to make union uniform
|
||||
null as namespace, -- Add to make union uniform
|
||||
null as ca_cert, -- Add to make union uniform
|
||||
null as tls_server_name, -- Add to make union uniform
|
||||
null as tls_skip_verify, -- Add to make union uniform
|
||||
null as worker_filter, -- Add to make union uniform
|
||||
null as token_hmac, -- Add to make union uniform
|
||||
null as token_status, -- Add to make union uniform
|
||||
null as client_cert, -- Add to make union uniform
|
||||
null as client_cert_key_hmac, -- Add to make union uniform
|
||||
'static' as subtype
|
||||
from static_stores
|
||||
)
|
||||
select *
|
||||
from final
|
||||
order by update_time desc, public_id asc;
|
||||
`
|
||||
|
||||
listStoresRefreshPageTemplate = `
|
||||
with stores as (
|
||||
select public_id
|
||||
from credential_store
|
||||
where update_time > @updated_after_time and
|
||||
(update_time, public_id) < (@last_item_update_time, @last_item_id) and
|
||||
%s -- search condition for project IDs is constructed
|
||||
order by update_time desc, public_id asc
|
||||
limit %d
|
||||
),
|
||||
vault_stores as (
|
||||
select *
|
||||
from credential_vault_store
|
||||
where public_id in (select public_id from stores)
|
||||
and delete_time is null
|
||||
),
|
||||
vault_tokens as (
|
||||
select store_id,
|
||||
token_hmac,
|
||||
status
|
||||
from credential_vault_token
|
||||
where store_id in (select public_id from stores)
|
||||
and status = 'current'
|
||||
),
|
||||
vault_client_certs as (
|
||||
select store_id,
|
||||
certificate,
|
||||
certificate_key_hmac
|
||||
from credential_vault_client_certificate
|
||||
where store_id in (select public_id from stores)
|
||||
),
|
||||
static_stores as (
|
||||
select *
|
||||
from credential_static_store
|
||||
where public_id in (select public_id from stores)
|
||||
),
|
||||
final as (
|
||||
select store.public_id,
|
||||
store.project_id,
|
||||
store.name,
|
||||
store.description,
|
||||
store.create_time,
|
||||
store.update_time,
|
||||
store.version,
|
||||
store.delete_time,
|
||||
store.vault_address,
|
||||
store.namespace,
|
||||
store.ca_cert,
|
||||
store.tls_server_name,
|
||||
store.tls_skip_verify,
|
||||
store.worker_filter,
|
||||
token.token_hmac as token_hmac,
|
||||
coalesce(token.status, 'expired') as token_status,
|
||||
cert.certificate as client_cert,
|
||||
cert.certificate_key_hmac as client_cert_key_hmac,
|
||||
'vault' as subtype
|
||||
from vault_stores store
|
||||
left join vault_tokens token on store.public_id = token.store_id
|
||||
left join vault_client_certs cert on store.public_id = cert.store_id
|
||||
union
|
||||
select public_id,
|
||||
project_id,
|
||||
name,
|
||||
description,
|
||||
create_time,
|
||||
update_time,
|
||||
version,
|
||||
null as delete_time, -- Add to make union uniform
|
||||
null as vault_address, -- Add to make union uniform
|
||||
null as namespace, -- Add to make union uniform
|
||||
null as ca_cert, -- Add to make union uniform
|
||||
null as tls_server_name, -- Add to make union uniform
|
||||
null as tls_skip_verify, -- Add to make union uniform
|
||||
null as worker_filter, -- Add to make union uniform
|
||||
null as token_hmac, -- Add to make union uniform
|
||||
null as token_status, -- Add to make union uniform
|
||||
null as client_cert, -- Add to make union uniform
|
||||
null as client_cert_key_hmac, -- Add to make union uniform
|
||||
'static' as subtype
|
||||
from static_stores
|
||||
)
|
||||
select *
|
||||
from final
|
||||
order by update_time desc, public_id asc;
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
)
|
||||
|
||||
// newStoreFunc is a function that creates a Store from the provided result.
|
||||
type newStoreFunc func(context.Context, *StoreListQueryResult) (Store, error)
|
||||
|
||||
// StoreSubtypeHooks defines the interface expected to be implemented
|
||||
// by credential store subtype hooks.
|
||||
type StoreSubtypeHooks interface {
|
||||
NewStore(context.Context, *StoreListQueryResult) (Store, error)
|
||||
}
|
||||
|
||||
type registry struct {
|
||||
m map[globals.Subtype]StoreSubtypeHooks
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (r *registry) set(s globals.Subtype, subtype StoreSubtypeHooks) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
_, previouslySet := r.m[s]
|
||||
if previouslySet {
|
||||
panic(fmt.Sprintf("credential store subtype %s already registered", s))
|
||||
}
|
||||
|
||||
r.m[s] = subtype
|
||||
}
|
||||
|
||||
func (r *registry) get(s globals.Subtype) (StoreSubtypeHooks, bool) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
subtype, ok := r.m[s]
|
||||
if ok {
|
||||
return subtype, ok
|
||||
}
|
||||
return nil, ok
|
||||
}
|
||||
|
||||
func (r *registry) newFunc(s globals.Subtype) (newStoreFunc, bool) {
|
||||
subtype, ok := r.get(s)
|
||||
if !ok {
|
||||
return nil, ok
|
||||
}
|
||||
return subtype.NewStore, ok
|
||||
}
|
||||
|
||||
var subtypeRegistry = registry{
|
||||
m: make(map[globals.Subtype]StoreSubtypeHooks),
|
||||
}
|
||||
|
||||
// RegisterStoreSubtype registers repository hooks for a provided store sub type.
|
||||
// RegisterStoreSubtype panics if the subtype has already been registered.
|
||||
func RegisterStoreSubtype(s globals.Subtype, hooks StoreSubtypeHooks) {
|
||||
subtypeRegistry.set(s, hooks)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/pagination"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// StoreRepository coordinates calls across different subtype services
|
||||
// to gather information about all credential stores.
|
||||
type StoreRepository struct {
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
}
|
||||
|
||||
// NewStoreRepository returns a new credential store repository.
|
||||
func NewStoreRepository(ctx context.Context, reader db.Reader, writer db.Writer) (*StoreRepository, error) {
|
||||
const op = "credential.NewStoreRepository"
|
||||
switch {
|
||||
case util.IsNil(reader):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing DB reader")
|
||||
case util.IsNil(writer):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing DB writer")
|
||||
}
|
||||
return &StoreRepository{
|
||||
reader: reader,
|
||||
writer: writer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List lists credential stores across all subtypes.
|
||||
func (s *StoreRepository) List(ctx context.Context, projectIds []string, afterItem pagination.Item, limit int) ([]Store, time.Time, error) {
|
||||
const op = "credential.(*StoreRepository).list"
|
||||
switch {
|
||||
case len(projectIds) == 0:
|
||||
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing project ids")
|
||||
case limit < 1:
|
||||
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing limit")
|
||||
}
|
||||
|
||||
var inClauses []string
|
||||
var args []any
|
||||
for i, projectId := range projectIds {
|
||||
arg := "project_id_" + strconv.Itoa(i)
|
||||
inClauses = append(inClauses, "@"+arg)
|
||||
args = append(args, sql.Named(arg, projectId))
|
||||
}
|
||||
inClause := strings.Join(inClauses, ", ")
|
||||
whereClause := "project_id in (" + inClause + ")"
|
||||
|
||||
query := fmt.Sprintf(listStoresTemplate, whereClause, limit)
|
||||
if afterItem != nil {
|
||||
query = fmt.Sprintf(listStoresPageTemplate, whereClause, limit)
|
||||
args = append(args,
|
||||
sql.Named("last_item_create_time", afterItem.GetCreateTime()),
|
||||
sql.Named("last_item_id", afterItem.GetPublicId()),
|
||||
)
|
||||
}
|
||||
|
||||
return s.queryStores(ctx, query, args)
|
||||
}
|
||||
|
||||
// ListRefresh lists credential stores across all subtypes.
|
||||
func (s *StoreRepository) ListRefresh(ctx context.Context, projectIds []string, updatedAfter time.Time, afterItem pagination.Item, limit int) ([]Store, time.Time, error) {
|
||||
const op = "credential.(*StoreRepository).list"
|
||||
switch {
|
||||
case len(projectIds) == 0:
|
||||
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing project ids")
|
||||
case updatedAfter.IsZero():
|
||||
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updated after time")
|
||||
case limit < 1:
|
||||
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing limit")
|
||||
}
|
||||
|
||||
var inClauses []string
|
||||
var args []any
|
||||
for i, projectId := range projectIds {
|
||||
arg := "project_id_" + strconv.Itoa(i)
|
||||
inClauses = append(inClauses, "@"+arg)
|
||||
args = append(args, sql.Named(arg, projectId))
|
||||
}
|
||||
inClause := strings.Join(inClauses, ", ")
|
||||
whereClause := "project_id in (" + inClause + ")"
|
||||
|
||||
query := fmt.Sprintf(listStoresRefreshTemplate, whereClause, limit)
|
||||
args = append(args,
|
||||
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
|
||||
)
|
||||
if afterItem != nil {
|
||||
query = fmt.Sprintf(listStoresRefreshPageTemplate, whereClause, limit)
|
||||
args = append(args,
|
||||
sql.Named("last_item_update_time", afterItem.GetUpdateTime()),
|
||||
sql.Named("last_item_id", afterItem.GetPublicId()),
|
||||
)
|
||||
}
|
||||
|
||||
return s.queryStores(ctx, query, args)
|
||||
}
|
||||
|
||||
// EstimatedCount estimates the total number of credential stores.
|
||||
func (s *StoreRepository) EstimatedCount(ctx context.Context) (int, error) {
|
||||
const op = "credential.(*StoreRepository).EstimatedCount"
|
||||
rows, err := s.reader.Query(ctx, estimateCountStoresQuery, nil)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total credential stores"))
|
||||
}
|
||||
var count int
|
||||
for rows.Next() {
|
||||
if err := s.reader.ScanRows(ctx, rows, &count); err != nil {
|
||||
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total credential stores"))
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ListDeletedIds lists the deleted IDs of all credential stores since the time specified.
|
||||
func (s *StoreRepository) ListDeletedIds(ctx context.Context, since time.Time) ([]string, time.Time, error) {
|
||||
const op = "credential.(*StoreRepository).ListDeletedIds"
|
||||
var deletedStoreIDs []string
|
||||
var transactionTimestamp time.Time
|
||||
if _, err := s.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
rows, err := s.writer.Query(ctx, listDeletedIdsQuery, []any{sql.Named("since", since)})
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
deletedStoreIDs = append(deletedStoreIDs, id)
|
||||
}
|
||||
transactionTimestamp, err = r.Now(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
return deletedStoreIDs, transactionTimestamp, nil
|
||||
}
|
||||
|
||||
func (s *StoreRepository) queryStores(ctx context.Context, query string, args []any) ([]Store, time.Time, error) {
|
||||
const op = "credential.(*StoreRepository).queryStores"
|
||||
|
||||
var stores []Store
|
||||
var transactionTimestamp time.Time
|
||||
if _, err := s.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
rows, err := r.Query(ctx, query, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
var foundStores []*StoreListQueryResult
|
||||
for rows.Next() {
|
||||
if err := r.ScanRows(ctx, rows, &foundStores); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range foundStores {
|
||||
store, err := s.toStore(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stores = append(stores, store)
|
||||
}
|
||||
transactionTimestamp, err = r.Now(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, time.Time{}, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return stores, transactionTimestamp, nil
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hashicorp/boundary/internal/credential"
|
||||
"github.com/hashicorp/boundary/internal/credential/static"
|
||||
sstore "github.com/hashicorp/boundary/internal/credential/static/store"
|
||||
"github.com/hashicorp/boundary/internal/credential/vault"
|
||||
vstore "github.com/hashicorp/boundary/internal/credential/vault/store"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestStoreRepository_List(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)
|
||||
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
|
||||
stores := []credential.Store{
|
||||
vault.TestCredentialStore(t, conn, wrapper, prj.GetPublicId(), "http://some-addr", "some-token", "some-accessor"),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()),
|
||||
vault.TestCredentialStore(t, conn, wrapper, prj.GetPublicId(), "http://some-addr", "some-token2", "some-accessor"),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()),
|
||||
}
|
||||
|
||||
// since we sort descending, we need to reverse the slice
|
||||
slices.Reverse(stores)
|
||||
|
||||
repo, err := credential.NewStoreRepository(ctx, rw, rw)
|
||||
require.NoError(err)
|
||||
|
||||
cmpOpts := []cmp.Option{
|
||||
cmpopts.IgnoreUnexported(
|
||||
vault.CredentialStore{},
|
||||
vstore.CredentialStore{},
|
||||
static.CredentialStore{},
|
||||
sstore.CredentialStore{},
|
||||
timestamp.Timestamp{},
|
||||
timestamppb.Timestamp{},
|
||||
),
|
||||
}
|
||||
|
||||
t.Run("validation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("missing project ids", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := repo.List(ctx, nil, nil, 1)
|
||||
require.ErrorContains(err, "missing project ids")
|
||||
})
|
||||
t.Run("invalid limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := repo.List(ctx, []string{prj.PublicId}, nil, 0)
|
||||
require.ErrorContains(err, "missing limit")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("success-without-after-item", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp, ttime, err := repo.List(ctx, []string{prj.PublicId}, nil, 10)
|
||||
require.NoError(err)
|
||||
// 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(resp, stores, cmpOpts...))
|
||||
})
|
||||
t.Run("success-with-after-item", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp, ttime, err := repo.List(ctx, []string{prj.PublicId}, stores[0], 10)
|
||||
require.NoError(err)
|
||||
// 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(resp, stores[1:], cmpOpts...))
|
||||
})
|
||||
}
|
||||
|
||||
func TestStoreRepository_ListRefresh(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)
|
||||
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
|
||||
|
||||
stores := []credential.Store{
|
||||
vault.TestCredentialStore(t, conn, wrapper, prj.GetPublicId(), "http://some-addr", "some-token", "some-accessor"),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()),
|
||||
vault.TestCredentialStore(t, conn, wrapper, prj.GetPublicId(), "http://some-addr", "some-token2", "some-accessor"),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()),
|
||||
}
|
||||
|
||||
// since we sort descending, we need to reverse the slice
|
||||
slices.Reverse(stores)
|
||||
|
||||
repo, err := credential.NewStoreRepository(ctx, rw, rw)
|
||||
require.NoError(err)
|
||||
|
||||
cmpOpts := []cmp.Option{
|
||||
cmpopts.IgnoreUnexported(
|
||||
vault.CredentialStore{},
|
||||
vstore.CredentialStore{},
|
||||
static.CredentialStore{},
|
||||
sstore.CredentialStore{},
|
||||
timestamp.Timestamp{},
|
||||
timestamppb.Timestamp{},
|
||||
),
|
||||
}
|
||||
|
||||
t.Run("validation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("missing updated after", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := repo.ListRefresh(ctx, []string{prj.PublicId}, time.Time{}, nil, 1)
|
||||
require.ErrorContains(err, "missing updated after time")
|
||||
})
|
||||
t.Run("missing project ids", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := repo.ListRefresh(ctx, nil, fiveDaysAgo, nil, 1)
|
||||
require.ErrorContains(err, "missing project ids")
|
||||
})
|
||||
t.Run("invalid limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := repo.ListRefresh(ctx, []string{prj.PublicId}, fiveDaysAgo, nil, 0)
|
||||
require.ErrorContains(err, "missing limit")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("success-without-after-item", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp, ttime, err := repo.ListRefresh(ctx, []string{prj.PublicId}, fiveDaysAgo, nil, 10)
|
||||
require.NoError(err)
|
||||
// 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(resp, stores, cmpOpts...))
|
||||
})
|
||||
t.Run("success-with-after-item", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp, ttime, err := repo.ListRefresh(ctx, []string{prj.PublicId}, fiveDaysAgo, stores[0], 10)
|
||||
require.NoError(err)
|
||||
// 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(resp, stores[1:], cmpOpts...))
|
||||
})
|
||||
t.Run("success-without-after-item-recent-updated-after", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp, ttime, err := repo.ListRefresh(ctx, []string{prj.PublicId}, stores[len(stores)-1].GetUpdateTime().AsTime(), nil, 10)
|
||||
require.NoError(err)
|
||||
// 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(resp, stores[:len(stores)-1], cmpOpts...))
|
||||
})
|
||||
t.Run("success-with-after-item-recent-updated-after", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp, ttime, err := repo.ListRefresh(ctx, []string{prj.PublicId}, stores[len(stores)-1].GetUpdateTime().AsTime(), stores[0], 10)
|
||||
require.NoError(err)
|
||||
// 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(resp, stores[1:len(stores)-1], cmpOpts...))
|
||||
})
|
||||
}
|
||||
|
||||
func TestStoreRepository_EstimatedCount(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)
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
staticRepo, err := static.NewRepository(ctx, rw, rw, kmsCache)
|
||||
require.NoError(err)
|
||||
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
|
||||
repo, err := credential.NewStoreRepository(ctx, rw, rw)
|
||||
require.NoError(err)
|
||||
|
||||
// Check total entries at start, expect 0
|
||||
numItems, err := repo.EstimatedCount(ctx)
|
||||
require.NoError(err)
|
||||
assert.Equal(0, numItems)
|
||||
|
||||
// Create some stores
|
||||
staticStores := static.TestCredentialStores(t, conn, wrapper, prj.PublicId, 2)
|
||||
_ = vault.TestCredentialStores(t, conn, wrapper, prj.PublicId, 2)
|
||||
// Run analyze to update postgres meta tables
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(err)
|
||||
|
||||
numItems, err = repo.EstimatedCount(ctx)
|
||||
require.NoError(err)
|
||||
assert.Equal(4, numItems)
|
||||
|
||||
// Delete a store
|
||||
_, err = staticRepo.DeleteCredentialStore(ctx, staticStores[0].GetPublicId())
|
||||
require.NoError(err)
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(err)
|
||||
|
||||
numItems, err = repo.EstimatedCount(ctx)
|
||||
require.NoError(err)
|
||||
assert.Equal(3, numItems)
|
||||
}
|
||||
|
||||
func TestRepository_ListDeletedStoreIds(t *testing.T) {
|
||||
t.Parallel()
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
ctx := context.Background()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
// Note: we're not testing vault credential stores, because they're deleted
|
||||
// asynchronously.
|
||||
staticStore := static.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0]
|
||||
|
||||
staticRepo, err := static.NewRepository(ctx, rw, rw, kms)
|
||||
require.NoError(err)
|
||||
repo, err := credential.NewStoreRepository(ctx, rw, rw)
|
||||
require.NoError(err)
|
||||
|
||||
// Expect no entries at the start
|
||||
deletedIds, ttime, err := repo.ListDeletedIds(ctx, time.Now().AddDate(-1, 0, 0))
|
||||
require.NoError(err)
|
||||
require.Empty(deletedIds)
|
||||
// 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)))
|
||||
|
||||
_, err = staticRepo.DeleteCredentialStore(ctx, staticStore.GetPublicId())
|
||||
require.NoError(err)
|
||||
|
||||
// Expect one entry
|
||||
deletedIds, ttime, err = repo.ListDeletedIds(ctx, time.Now().AddDate(-1, 0, 0))
|
||||
require.NoError(err)
|
||||
assert.Empty(
|
||||
cmp.Diff(
|
||||
[]string{staticStore.GetPublicId()},
|
||||
deletedIds,
|
||||
cmpopts.SortSlices(func(i, j string) bool { return i < j }),
|
||||
),
|
||||
)
|
||||
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
|
||||
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
|
||||
|
||||
// Try again with the time set to now, expect no entries
|
||||
deletedIds, ttime, err = repo.ListDeletedIds(ctx, time.Now())
|
||||
require.NoError(err)
|
||||
require.Empty(deletedIds)
|
||||
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
|
||||
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/pagination"
|
||||
)
|
||||
|
||||
// ListStores lists up to page size credential stores, filtering out entries that
|
||||
// do not pass the filter item function. It will automatically request
|
||||
// more credential stores from the database, at page size chunks, to fill the page.
|
||||
// It returns a new list token used to continue pagination or refresh items.
|
||||
// Credential stores are ordered by create time descending (most recently created first).
|
||||
func ListStores(
|
||||
ctx context.Context,
|
||||
grantsHash []byte,
|
||||
pageSize int,
|
||||
filterItemFn pagination.ListFilterFunc[Store],
|
||||
repo *StoreRepository,
|
||||
projectIds []string,
|
||||
) (*pagination.ListResponse[Store], error) {
|
||||
const op = "credential.ListStores"
|
||||
|
||||
switch {
|
||||
case len(grantsHash) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
||||
case pageSize < 1:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
|
||||
case filterItemFn == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
|
||||
case projectIds == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project ids")
|
||||
case repo == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
|
||||
}
|
||||
|
||||
listItemsFn := func(ctx context.Context, lastPageItem Store, limit int) ([]Store, time.Time, error) {
|
||||
return repo.List(ctx, projectIds, lastPageItem, limit)
|
||||
}
|
||||
|
||||
return pagination.List(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.EstimatedCount)
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"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/static"
|
||||
sstore "github.com/hashicorp/boundary/internal/credential/static/store"
|
||||
"github.com/hashicorp/boundary/internal/credential/vault"
|
||||
vstore "github.com/hashicorp/boundary/internal/credential/vault/store"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/listtoken"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type fakeReader struct {
|
||||
db.Reader
|
||||
}
|
||||
type fakeWriter struct {
|
||||
db.Writer
|
||||
}
|
||||
|
||||
func TestNewStoreService(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
t.Run("success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := credential.NewStoreRepository(ctx, &fakeReader{}, &fakeWriter{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
})
|
||||
t.Run("nil-reader", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := credential.NewStoreRepository(ctx, nil, &fakeWriter{})
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("nil-interface-reader", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := credential.NewStoreRepository(ctx, (*fakeReader)(nil), &fakeWriter{})
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("nil-writer", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := credential.NewStoreRepository(ctx, &fakeReader{}, nil)
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("nil-interface-writer", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := credential.NewStoreRepository(ctx, &fakeReader{}, (*fakeWriter)(nil))
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStoreService_List(t *testing.T) {
|
||||
// Set database read timeout to avoid duplicates in response
|
||||
oldReadTimeout := globals.RefreshReadLookbackDuration
|
||||
globals.RefreshReadLookbackDuration = 0
|
||||
t.Cleanup(func() {
|
||||
globals.RefreshReadLookbackDuration = oldReadTimeout
|
||||
})
|
||||
ctx := context.Background()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
sqlDb, err := conn.SqlDB(ctx)
|
||||
require.NoError(t, err)
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
||||
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
|
||||
stores := []credential.Store{
|
||||
vault.TestCredentialStore(t, conn, wrapper, prj.PublicId, "http://some-addr", "some-token", "some-accessor"),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.PublicId),
|
||||
vault.TestCredentialStore(t, conn, wrapper, prj.PublicId, "http://some-addr", "some-token2", "some-accessor"),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.PublicId),
|
||||
static.TestCredentialStore(t, conn, wrapper, prj.PublicId),
|
||||
}
|
||||
|
||||
// since we sort descending, we need to reverse the slice
|
||||
slices.Reverse(stores)
|
||||
|
||||
staticRepo, err := static.NewRepository(ctx, rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
repo, err := credential.NewStoreRepository(ctx, rw, rw)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Run analyze to update count estimate
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(t, err)
|
||||
|
||||
cmpOpts := []cmp.Option{
|
||||
cmpopts.IgnoreUnexported(
|
||||
vault.CredentialStore{},
|
||||
vstore.CredentialStore{},
|
||||
static.CredentialStore{},
|
||||
sstore.CredentialStore{},
|
||||
timestamp.Timestamp{},
|
||||
timestamppb.Timestamp{},
|
||||
),
|
||||
}
|
||||
|
||||
t.Run("List validation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("missing grants hash", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
_, err := credential.ListStores(ctx, nil, 1, filterFunc, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing grants hash")
|
||||
})
|
||||
t.Run("zero page size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
_, err := credential.ListStores(ctx, []byte("some hash"), 0, filterFunc, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "page size must be at least 1")
|
||||
})
|
||||
t.Run("negative page size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
_, err := credential.ListStores(ctx, []byte("some hash"), -1, filterFunc, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "page size must be at least 1")
|
||||
})
|
||||
t.Run("nil filter func", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := credential.ListStores(ctx, []byte("some hash"), 1, nil, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing filter item callback")
|
||||
})
|
||||
t.Run("missing repo", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
_, err := credential.ListStores(ctx, []byte("some hash"), 1, filterFunc, nil, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing repo")
|
||||
})
|
||||
t.Run("missing public Ids", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
_, err := credential.ListStores(ctx, []byte("some hash"), 1, filterFunc, repo, nil)
|
||||
require.ErrorContains(t, err, "missing project ids")
|
||||
})
|
||||
})
|
||||
t.Run("ListPage validation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("missing grants hash", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), "some-id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresPage(ctx, nil, 1, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing grants hash")
|
||||
})
|
||||
t.Run("zero page size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), "some-id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "page size must be at least 1")
|
||||
})
|
||||
t.Run("negative page size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), "some-id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "page size must be at least 1")
|
||||
})
|
||||
t.Run("nil filter func", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), "some-id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresPage(ctx, []byte("some hash"), 1, nil, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing filter item callback")
|
||||
})
|
||||
t.Run("nil token", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
_, err = credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing token")
|
||||
})
|
||||
t.Run("wrong token type", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "token did not have a pagination token component")
|
||||
})
|
||||
t.Run("missing repo", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), "some-id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing repo")
|
||||
})
|
||||
t.Run("missing project ids", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), "some-id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil)
|
||||
require.ErrorContains(t, err, "missing project ids")
|
||||
})
|
||||
})
|
||||
t.Run("ListRefresh validation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("missing grants hash", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefresh(ctx, nil, 1, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing grants hash")
|
||||
})
|
||||
t.Run("zero page size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefresh(ctx, []byte("some hash"), 0, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "page size must be at least 1")
|
||||
})
|
||||
t.Run("negative page size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefresh(ctx, []byte("some hash"), -1, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "page size must be at least 1")
|
||||
})
|
||||
t.Run("nil filter func", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefresh(ctx, []byte("some hash"), 1, nil, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing filter item callback")
|
||||
})
|
||||
t.Run("nil token", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
_, err = credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, nil, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing token")
|
||||
})
|
||||
t.Run("missing repo", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, nil, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing repo")
|
||||
})
|
||||
t.Run("missing project ids", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil)
|
||||
require.ErrorContains(t, err, "missing project ids")
|
||||
})
|
||||
})
|
||||
t.Run("ListRefreshPage validation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("missing grants hash", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefreshPage(ctx, nil, 1, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing grants hash")
|
||||
})
|
||||
t.Run("zero page size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefreshPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "page size must be at least 1")
|
||||
})
|
||||
t.Run("negative page size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefreshPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "page size must be at least 1")
|
||||
})
|
||||
t.Run("nil filter func", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefreshPage(ctx, []byte("some hash"), 1, nil, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing filter item callback")
|
||||
})
|
||||
t.Run("nil token", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
_, err = credential.ListStoresRefreshPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing token")
|
||||
})
|
||||
t.Run("wrong token type", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), "some-id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "token did not have a refresh token component")
|
||||
})
|
||||
t.Run("missing repo", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, []string{prj.PublicId})
|
||||
require.ErrorContains(t, err, "missing repo")
|
||||
})
|
||||
t.Run("missing project ids", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filterFunc := func(_ context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.CredentialLibrary, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
|
||||
require.NoError(t, err)
|
||||
_, err = credential.ListStoresRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil)
|
||||
require.ErrorContains(t, err, "missing project ids")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("simple pagination", func(t *testing.T) {
|
||||
filterFunc := func(context.Context, credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
resp, err := credential.ListStores(ctx, []byte("some hash"), 1, filterFunc, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.ListToken)
|
||||
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.False(t, resp.CompleteListing)
|
||||
require.Equal(t, resp.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp.DeletedIds)
|
||||
require.Len(t, resp.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp.Items[0], stores[0], cmpOpts...))
|
||||
|
||||
resp2, err := credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.False(t, resp2.CompleteListing)
|
||||
require.Equal(t, resp2.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp2.DeletedIds)
|
||||
require.Len(t, resp2.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp2.Items[0], stores[1], cmpOpts...))
|
||||
|
||||
resp3, err := credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.False(t, resp3.CompleteListing)
|
||||
require.Equal(t, resp3.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp3.DeletedIds)
|
||||
require.Len(t, resp3.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp3.Items[0], stores[2], cmpOpts...))
|
||||
|
||||
resp4, err := credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.False(t, resp4.CompleteListing)
|
||||
require.Equal(t, resp4.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp4.DeletedIds)
|
||||
require.Len(t, resp4.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp4.Items[0], stores[3], cmpOpts...))
|
||||
|
||||
resp5, err := credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp5.CompleteListing)
|
||||
require.Equal(t, resp5.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp5.DeletedIds)
|
||||
require.Len(t, resp5.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp5.Items[0], stores[4], cmpOpts...))
|
||||
|
||||
// Finished initial pagination phase, request refresh
|
||||
// Expect no results.
|
||||
resp6, err := credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, resp5.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp6.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp6.CompleteListing)
|
||||
require.Equal(t, resp6.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp6.DeletedIds)
|
||||
require.Empty(t, resp6.Items)
|
||||
|
||||
// Create some new credential stores
|
||||
newStore1 := static.TestCredentialStore(t, conn, wrapper, prj.PublicId)
|
||||
newStore2 := static.TestCredentialStore(t, conn, wrapper, prj.PublicId)
|
||||
t.Cleanup(func() {
|
||||
_, err := staticRepo.DeleteCredentialStore(ctx, newStore1.PublicId)
|
||||
require.NoError(t, err)
|
||||
_, err = staticRepo.DeleteCredentialStore(ctx, newStore2.PublicId)
|
||||
require.NoError(t, err)
|
||||
// Run analyze to update count estimate
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
// Run analyze to update count estimate
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Refresh again, should get newStore2
|
||||
resp7, err := credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, resp6.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.False(t, resp7.CompleteListing)
|
||||
require.Equal(t, resp7.EstimatedItemCount, 7)
|
||||
require.Empty(t, resp7.DeletedIds)
|
||||
require.Len(t, resp7.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp7.Items[0], newStore2, cmpOpts...))
|
||||
|
||||
// Refresh again, should get newStore1
|
||||
resp8, err := credential.ListStoresRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp7.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp8.CompleteListing)
|
||||
require.Equal(t, resp8.EstimatedItemCount, 7)
|
||||
require.Empty(t, resp8.DeletedIds)
|
||||
require.Len(t, resp8.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp8.Items[0], newStore1, cmpOpts...))
|
||||
|
||||
// Refresh again, should get no results
|
||||
resp9, err := credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, resp8.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp9.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp9.CompleteListing)
|
||||
require.Equal(t, resp9.EstimatedItemCount, 7)
|
||||
require.Empty(t, resp9.DeletedIds)
|
||||
require.Empty(t, resp9.Items)
|
||||
})
|
||||
|
||||
t.Run("simple pagination with aggressive filtering", func(t *testing.T) {
|
||||
filterFunc := func(ctx context.Context, s credential.Store) (bool, error) {
|
||||
return s.GetPublicId() == stores[1].GetPublicId() ||
|
||||
s.GetPublicId() == stores[len(stores)-1].GetPublicId(), nil
|
||||
}
|
||||
resp, err := credential.ListStores(ctx, []byte("some hash"), 1, filterFunc, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.ListToken)
|
||||
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.False(t, resp.CompleteListing)
|
||||
require.Equal(t, resp.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp.DeletedIds)
|
||||
require.Len(t, resp.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp.Items[0], stores[1], cmpOpts...))
|
||||
|
||||
resp2, err := credential.ListStoresPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp2.ListToken)
|
||||
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp2.CompleteListing)
|
||||
require.Equal(t, resp2.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp2.DeletedIds)
|
||||
require.Len(t, resp2.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp2.Items[0], stores[len(stores)-1], cmpOpts...))
|
||||
|
||||
// request a refresh, nothing should be returned
|
||||
resp3, err := credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp3.CompleteListing)
|
||||
require.Equal(t, resp3.EstimatedItemCount, 5)
|
||||
require.Empty(t, resp3.DeletedIds)
|
||||
require.Empty(t, resp3.Items)
|
||||
|
||||
// Create some new credential stores
|
||||
newStore1 := static.TestCredentialStore(t, conn, wrapper, prj.PublicId)
|
||||
newStore2 := static.TestCredentialStore(t, conn, wrapper, prj.PublicId)
|
||||
newStore3 := static.TestCredentialStore(t, conn, wrapper, prj.PublicId)
|
||||
newStore4 := static.TestCredentialStore(t, conn, wrapper, prj.PublicId)
|
||||
t.Cleanup(func() {
|
||||
_, err := staticRepo.DeleteCredentialStore(ctx, newStore1.PublicId)
|
||||
require.NoError(t, err)
|
||||
_, err = staticRepo.DeleteCredentialStore(ctx, newStore2.PublicId)
|
||||
require.NoError(t, err)
|
||||
_, err = staticRepo.DeleteCredentialStore(ctx, newStore3.PublicId)
|
||||
require.NoError(t, err)
|
||||
_, err = staticRepo.DeleteCredentialStore(ctx, newStore4.PublicId)
|
||||
require.NoError(t, err)
|
||||
// Run analyze to update count estimate
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
// Run analyze to update count estimate
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(t, err)
|
||||
|
||||
filterFunc = func(ctx context.Context, s credential.Store) (bool, error) {
|
||||
return s.GetPublicId() == newStore1.GetPublicId() ||
|
||||
s.GetPublicId() == newStore3.GetPublicId(), nil
|
||||
}
|
||||
// Refresh again, should get newStore3
|
||||
resp4, err := credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.False(t, resp4.CompleteListing)
|
||||
require.Equal(t, resp4.EstimatedItemCount, 9)
|
||||
require.Empty(t, resp4.DeletedIds)
|
||||
require.Len(t, resp4.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp4.Items[0], newStore3, cmpOpts...))
|
||||
|
||||
// Refresh again, should get newStore1
|
||||
resp5, err := credential.ListStoresRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp5.CompleteListing)
|
||||
require.Equal(t, resp5.EstimatedItemCount, 9)
|
||||
require.Empty(t, resp5.DeletedIds)
|
||||
require.Len(t, resp5.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp5.Items[0], newStore1, cmpOpts...))
|
||||
})
|
||||
|
||||
t.Run("simple pagination with deletion", func(t *testing.T) {
|
||||
filterFunc := func(ctx context.Context, s credential.Store) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
deletedStoreId := stores[0].GetPublicId()
|
||||
_, err := staticRepo.DeleteCredentialStore(ctx, deletedStoreId)
|
||||
require.NoError(t, err)
|
||||
stores = stores[1:]
|
||||
|
||||
// Run analyze to update count estimate
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := credential.ListStores(ctx, []byte("some hash"), 1, filterFunc, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.ListToken)
|
||||
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.False(t, resp.CompleteListing)
|
||||
require.Equal(t, resp.EstimatedItemCount, 4)
|
||||
require.Empty(t, resp.DeletedIds)
|
||||
require.Len(t, resp.Items, 1)
|
||||
require.Empty(t, cmp.Diff(resp.Items[0], stores[0], cmpOpts...))
|
||||
|
||||
// request remaining results
|
||||
resp2, err := credential.ListStoresPage(ctx, []byte("some hash"), 3, filterFunc, resp.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp2.CompleteListing)
|
||||
require.Equal(t, resp2.EstimatedItemCount, 4)
|
||||
require.Empty(t, resp2.DeletedIds)
|
||||
require.Len(t, resp2.Items, 3)
|
||||
require.Empty(t, cmp.Diff(resp2.Items, stores[1:], cmpOpts...))
|
||||
|
||||
deletedStoreId = stores[0].GetPublicId()
|
||||
_, err = staticRepo.DeleteCredentialStore(ctx, deletedStoreId)
|
||||
require.NoError(t, err)
|
||||
stores = stores[1:]
|
||||
|
||||
// Run analyze to update count estimate
|
||||
_, err = sqlDb.ExecContext(ctx, "analyze")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp3, err := credential.ListStoresRefresh(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, []string{prj.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
|
||||
require.True(t, resp3.CompleteListing)
|
||||
require.Equal(t, resp3.EstimatedItemCount, 3)
|
||||
require.Contains(t, resp3.DeletedIds, deletedStoreId)
|
||||
require.Empty(t, resp3.Items)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/listtoken"
|
||||
"github.com/hashicorp/boundary/internal/pagination"
|
||||
)
|
||||
|
||||
// ListStoresPage lists up to page size credential stores, filtering out entries that
|
||||
// do not pass the filter item function. It will automatically request
|
||||
// more credential stores from the database, at page size chunks, to fill the page.
|
||||
// It will start its paging based on the information in the token.
|
||||
// It returns a new list token used to continue pagination or refresh items.
|
||||
// Credential stores are ordered by create time descending (most recently created first).
|
||||
func ListStoresPage(
|
||||
ctx context.Context,
|
||||
grantsHash []byte,
|
||||
pageSize int,
|
||||
filterItemFn pagination.ListFilterFunc[Store],
|
||||
tok *listtoken.Token,
|
||||
repo *StoreRepository,
|
||||
projectIds []string,
|
||||
) (*pagination.ListResponse[Store], error) {
|
||||
const op = "credential.ListStoresPage"
|
||||
|
||||
switch {
|
||||
case len(grantsHash) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
||||
case pageSize < 1:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
|
||||
case filterItemFn == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
|
||||
case tok == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
|
||||
case repo == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
|
||||
}
|
||||
if _, ok := tok.Subtype.(*listtoken.PaginationToken); !ok {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a pagination token component")
|
||||
}
|
||||
|
||||
listItemsFn := func(ctx context.Context, lastPageItem Store, limit int) ([]Store, time.Time, error) {
|
||||
if lastPageItem != nil {
|
||||
return repo.List(ctx, projectIds, lastPageItem, limit)
|
||||
}
|
||||
lastItem, err := tok.LastItem(ctx)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
return repo.List(ctx, projectIds, lastItem, limit)
|
||||
}
|
||||
|
||||
return pagination.ListPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.EstimatedCount, tok)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/listtoken"
|
||||
"github.com/hashicorp/boundary/internal/pagination"
|
||||
)
|
||||
|
||||
// ListStoresRefresh lists up to page size credential stores, filtering out entries that
|
||||
// do not pass the filter item function. It will automatically request
|
||||
// more credential stores from the database, at page size chunks, to fill the page.
|
||||
// It will start its paging based on the information in the token.
|
||||
// It returns a new list token used to continue pagination or refresh items.
|
||||
// Credential stores are ordered by update time descending (most recently updated first).
|
||||
// Credential stores may contain items that were already returned during the initial
|
||||
// pagination phase. It also returns a list of any credential stores deleted since the
|
||||
// start of the initial pagination phase or last response.
|
||||
func ListStoresRefresh(
|
||||
ctx context.Context,
|
||||
grantsHash []byte,
|
||||
pageSize int,
|
||||
filterItemFn pagination.ListFilterFunc[Store],
|
||||
tok *listtoken.Token,
|
||||
repo *StoreRepository,
|
||||
projectIds []string,
|
||||
) (*pagination.ListResponse[Store], error) {
|
||||
const op = "credential.ListStoresRefresh"
|
||||
|
||||
switch {
|
||||
case len(grantsHash) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
||||
case pageSize < 1:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
|
||||
case filterItemFn == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
|
||||
case tok == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
|
||||
case projectIds == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project ids")
|
||||
case repo == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
|
||||
}
|
||||
rt, ok := tok.Subtype.(*listtoken.StartRefreshToken)
|
||||
if !ok {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a start-refresh token component")
|
||||
}
|
||||
|
||||
listItemsFn := func(ctx context.Context, lastPageItem Store, limit int) ([]Store, time.Time, error) {
|
||||
// Add the database read timeout to account for any creations missed due to concurrent
|
||||
// transactions in the initial pagination phase.
|
||||
return repo.ListRefresh(ctx, projectIds, rt.PreviousPhaseUpperBound.Add(-globals.RefreshReadLookbackDuration), lastPageItem, limit)
|
||||
}
|
||||
listDeletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) {
|
||||
// Add the database read timeout to account for any deletions missed due to concurrent
|
||||
// transactions in previous requests.
|
||||
since = since.Add(-globals.RefreshReadLookbackDuration)
|
||||
return repo.ListDeletedIds(ctx, since)
|
||||
}
|
||||
|
||||
return pagination.ListRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.EstimatedCount, listDeletedIDsFn, tok)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/listtoken"
|
||||
"github.com/hashicorp/boundary/internal/pagination"
|
||||
)
|
||||
|
||||
// ListStoresRefreshPage lists up to page size credential stores, filtering out entries that
|
||||
// do not pass the filter item function. It will automatically request
|
||||
// more credential stores from the database, at page size chunks, to fill the page.
|
||||
// It will start its paging based on the information in the token.
|
||||
// It returns a new list token used to continue pagination or refresh items.
|
||||
// Credential stores are ordered by update time descending (most recently updated first).
|
||||
// Credential stores may contain items that were already returned during the initial
|
||||
// pagination phase. It also returns a list of any credential stores deleted since the
|
||||
// last response.
|
||||
func ListStoresRefreshPage(
|
||||
ctx context.Context,
|
||||
grantsHash []byte,
|
||||
pageSize int,
|
||||
filterItemFn pagination.ListFilterFunc[Store],
|
||||
tok *listtoken.Token,
|
||||
repo *StoreRepository,
|
||||
projectIds []string,
|
||||
) (*pagination.ListResponse[Store], error) {
|
||||
const op = "credential.ListStoresRefreshPage"
|
||||
|
||||
switch {
|
||||
case len(grantsHash) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
||||
case pageSize < 1:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
|
||||
case filterItemFn == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
|
||||
case tok == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
|
||||
case repo == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
|
||||
}
|
||||
rt, ok := tok.Subtype.(*listtoken.RefreshToken)
|
||||
if !ok {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a refresh token component")
|
||||
}
|
||||
|
||||
listItemsFn := func(ctx context.Context, lastPageItem Store, limit int) ([]Store, time.Time, error) {
|
||||
// Add the database read timeout to account for any creations missed due to concurrent
|
||||
// transactions in the initial pagination phase.
|
||||
since := rt.PhaseLowerBound.Add(-globals.RefreshReadLookbackDuration)
|
||||
if lastPageItem != nil {
|
||||
return repo.ListRefresh(ctx, projectIds, since, lastPageItem, limit)
|
||||
}
|
||||
lastItem, err := tok.LastItem(ctx)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
return repo.ListRefresh(ctx, projectIds, since, lastItem, limit)
|
||||
}
|
||||
listDeletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) {
|
||||
// Add the database read timeout to account for any deletes missed due to concurrent
|
||||
// transactions in the original list pagination phase.
|
||||
return repo.ListDeletedIds(ctx, since.Add(-globals.RefreshReadLookbackDuration))
|
||||
}
|
||||
|
||||
return pagination.ListRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.EstimatedCount, listDeletedIDsFn, tok)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
// StoreListQueryResult describes the result from the
|
||||
// credential store list query used to list all credential
|
||||
// store subtypes.
|
||||
type StoreListQueryResult struct {
|
||||
// PublicId is a surrogate key suitable for use in a public API.
|
||||
PublicId string `gorm:"primary_key"`
|
||||
// The Project Id of the owning project and must be set.
|
||||
ProjectId string
|
||||
// Optional name of the credential store.
|
||||
Name string
|
||||
// Optional description of the credential store.
|
||||
Description string
|
||||
// Create time of the credential store.
|
||||
CreateTime *timestamp.Timestamp
|
||||
// Update time of the credential store.
|
||||
UpdateTime *timestamp.Timestamp
|
||||
// Version of the credential store.
|
||||
Version uint32
|
||||
// Optional delete time of the credential store.
|
||||
DeleteTime *timestamp.Timestamp
|
||||
// Optional Vault address of the credential store.
|
||||
VaultAddress string
|
||||
// Optional namespace of the credential store.
|
||||
Namespace string
|
||||
// Optional CA cert of the credential store.
|
||||
CaCert []byte
|
||||
// Optional TLS server name of the credential store.
|
||||
TlsServerName string
|
||||
// Optionally specifices whether to skip TLS verification of the credential store.
|
||||
TlsSkipVerify bool
|
||||
// Optional worker filter of the credential store.
|
||||
WorkerFilter string
|
||||
// Optional token HMAC of the credential store.
|
||||
TokenHmac []byte
|
||||
// Optional token status of the credential store.
|
||||
TokenStatus string
|
||||
// Optional client certificate of the credential store.
|
||||
ClientCert []byte
|
||||
// Optional client cert key HMAC of the credential store.
|
||||
ClientCertKeyHmac []byte
|
||||
// The subtype of the credential store.
|
||||
Subtype string
|
||||
}
|
||||
|
||||
func (s *StoreListQueryResult) toStore(ctx context.Context) (Store, error) {
|
||||
const op = "credential.(*StoreListQueryResult).toStore"
|
||||
|
||||
newFn, ok := subtypeRegistry.newFunc(globals.Subtype(s.Subtype))
|
||||
if !ok {
|
||||
return nil, errors.New(ctx,
|
||||
errors.InvalidParameter,
|
||||
op,
|
||||
fmt.Sprintf("%s is an unknown credential store subtype of %s", s.PublicId, s.Subtype),
|
||||
)
|
||||
}
|
||||
|
||||
return newFn(ctx, s)
|
||||
}
|
||||
@@ -6,6 +6,8 @@ syntax = "proto3";
|
||||
// Package store provides protobufs for storing types in the credential package.
|
||||
package controller.storage.credential.store.v1;
|
||||
|
||||
import "controller/storage/timestamp/v1/timestamp.proto";
|
||||
|
||||
option go_package = "github.com/hashicorp/boundary/internal/credential/store;store";
|
||||
|
||||
message Store {
|
||||
@@ -58,3 +60,64 @@ message Dynamic {
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
string library_id = 2;
|
||||
}
|
||||
|
||||
message StoreListQueryResult {
|
||||
// public_id is a surrogate key suitable for use in a public API.
|
||||
// @inject_tag: `gorm:"primary_key"`
|
||||
string public_id = 1;
|
||||
|
||||
// The project_id of the owning project and must be set.
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
string project_id = 2;
|
||||
|
||||
// Optional name of the credential store.
|
||||
string name = 3;
|
||||
|
||||
// Optional description of the credential store.
|
||||
string description = 4;
|
||||
|
||||
// Create time of the credential store.
|
||||
timestamp.v1.Timestamp create_time = 5;
|
||||
|
||||
// Update time of the credential store.
|
||||
timestamp.v1.Timestamp update_time = 6;
|
||||
|
||||
// Version of the credential store.
|
||||
uint32 version = 7;
|
||||
|
||||
// Optional delete time of the credential store.
|
||||
timestamp.v1.Timestamp delete_time = 8;
|
||||
|
||||
// Optional Vault address of the credential store.
|
||||
string vault_address = 9;
|
||||
|
||||
// Optional namespace of the credential store.
|
||||
string namespace = 10;
|
||||
|
||||
// Optional CA cert of the credential store.
|
||||
bytes ca_cert = 11;
|
||||
|
||||
// Optional TLS server name of the credential store.
|
||||
string tls_server_name = 12;
|
||||
|
||||
// Optionally specifices whether to skip TLS verification of the credential store.
|
||||
bool tls_skip_verify = 13;
|
||||
|
||||
// Optional worker filter of the credential store.
|
||||
string worker_filter = 14;
|
||||
|
||||
// Optional token HMAC of the credential store.
|
||||
bytes token_hmac = 15;
|
||||
|
||||
// Optional token status of the credential store.
|
||||
string token_status = 16;
|
||||
|
||||
// Optional client certificate of the credential store.
|
||||
bytes client_cert = 17;
|
||||
|
||||
// Optional client cert key HMAC of the credential store.
|
||||
bytes client_cert_key_hmac = 18;
|
||||
|
||||
// The subtype of the credential store.
|
||||
string subtype = 19;
|
||||
}
|
||||
Reference in new issue
Block a user