Auth Method list pagination (#4165)

* wip

* implement list pagination for auth methods

* remove unused code

* update tests to fix sorting

* address PR comments

* simplify query, add token tests

* change test name

* wip sql testing

* add boolean column to indicate whether auth methods should be returned to unauthenticated users, remove secrets from api response

* is_active_public not null

* remove secrets and unwrapping

* move not null to immediately after data migration

* order public_id descending

* fix(db): Fix auth method migrations (#4193)

* wip

* fix db tests

* fix failing api tests

* reverse order of create time and update time in test

---------

Co-authored-by: Michael Gaffney <mike@gaffney.cc>
This commit is contained in:
Michael MiltonandMichael Gaffney authored and Johan Brandhorst-Satzkorn committed 2024-01-05 07:43:21 -08:00
1 parent 0fad770d8a
commit 8d50a0913f
34 files changed
+4340 -445

No files matched your search

@@ -216,13 +216,16 @@ func TestSelfReadingDifferentOutputFields(t *testing.T) {
conn := tc.DbConn()
s, err := authmethodsservice.NewService(tc.Context(),
s, err := authmethodsservice.NewService(
tc.Context(),
tc.Kms(),
tc.Controller().PasswordAuthRepoFn,
tc.Controller().OidcRepoFn,
tc.Controller().IamRepoFn,
tc.Controller().AuthTokenRepoFn,
tc.Controller().LdapRepoFn,
tc.Controller().AuthMethodRepoFn,
1000,
)
require.NoError(t, err)
+94
View File
@@ -0,0 +1,94 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
"fmt"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
)
// AuthMethodListQueryResult describes the result from the
// auth method list query used to list all auth method subtypes.
type AuthMethodListQueryResult struct {
// PublicId is a surrogate key suitable for use in a public API.
PublicId string `gorm:"primary_key"`
// The Scope Id of the scope this auth method belongs to, must be set.
ScopeId string
// Sets the primary subtype of the auth method.
IsPrimaryAuthMethod bool
// Optional name of the auth method.
Name string
// Optional description of the auth method.
Description string
// Create time of the auth method.
CreateTime *timestamp.Timestamp
// Update time of the auth method.
UpdateTime *timestamp.Timestamp
// Version of the auth method.
Version uint32
// Optionally set by ldap or oidc auth methods.
State string
Certs string
// Optionally set by ldap auth method.
StartTLS bool
InsecureTLS bool
DiscoverDn bool
AnonGroupSearch bool
UpnDomain string
Urls string
UserDn string
UserAttr string
UserFilter string
EnableGroups bool
UseTokenGroups bool
GroupDn string
GroupAttr string
GroupFilter string
ClientCertificateKeyHmac []byte
ClientCertificateKeyId string
ClientCertificateCert []byte
BindDn string
BindKeyId string
AccountAttributeMap string
DereferenceAliases string
MaximumPageSize uint32
// Optionally set by oidc auth method.
DisableDiscoveredConfigValidation bool
Issuer string
ClientId string
ClientSecretHmac string
KeyId string
MaxAge int
Algs string
ApiUrl string
Auds string
ClaimsScopes string
AccountClaimMaps string
Prompts string
// Optionally set by password auth method.
PasswordConfId string
MinLoginNameLength uint32
MinPasswordLength uint32
// The subtype of the auth method.
Subtype string
}
func (am *AuthMethodListQueryResult) toAuthMethod(ctx context.Context) (AuthMethod, error) {
const op = "auth.(*AuthMethodListQueryResult).toAuthMethod"
newFn, ok := subtypeRegistry.newFunc(globals.Subtype(am.Subtype))
if !ok {
return nil, errors.New(ctx,
errors.InvalidParameter,
op,
fmt.Sprintf("%s is an unknown auth method subtype of %s", am.PublicId, am.Subtype),
)
}
return newFn(ctx, am)
}
+18 -4
View File
@@ -9,6 +9,7 @@ import (
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -20,14 +21,17 @@ func TestDB_AuthMethodIDTrigger(t *testing.T) {
create table if not exists test_auth_method (
public_id wt_public_id primary key,
scope_id wt_public_id not null references iam_scope(public_id),
name wt_name
name wt_name,
create_time wt_timestamp,
update_time wt_timestamp,
state text
);
`
insert = `
insert into test_auth_method
(public_id, scope_id)
(public_id, scope_id, create_time, update_time, state)
values
(@public_id, @scoped_id);
(@public_id, @scoped_id, @create_time, @update_time, @state);
`
addTriggers = `
create trigger
@@ -59,7 +63,17 @@ select count(*) from test_auth_method where public_id = @public_id
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
id := "l1Ocw0TpHn800CekIxIXlmQqRDgFDfYl"
inserted, err := rw.Exec(ctx, insert, []any{sql.Named("public_id", id), sql.Named("scoped_id", org.GetPublicId())})
inserted, err := rw.Exec(
ctx,
insert,
[]any{
sql.Named("public_id", id),
sql.Named("scoped_id", org.GetPublicId()),
sql.Named("create_time", timestamp.Now()),
sql.Named("update_time", timestamp.Now()),
sql.Named("state", "active-public"),
},
)
require.NoError(err)
require.Equal(1, inserted)
+62
View File
@@ -0,0 +1,62 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap
import (
"context"
"strings"
"github.com/hashicorp/boundary/internal/auth"
)
func init() {
auth.RegisterAuthMethodSubtype("ldap", &authMethodHooks{})
}
type authMethodHooks struct{}
// NewAuthMethod creates a new static auth method from the result
func (authMethodHooks) NewAuthMethod(ctx context.Context, result *auth.AuthMethodListQueryResult) (auth.AuthMethod, error) {
delimiter := "|"
am := AllocAuthMethod()
am.PublicId = result.PublicId
am.ScopeId = result.ScopeId
am.IsPrimaryAuthMethod = result.IsPrimaryAuthMethod
am.Name = result.Name
am.Description = result.Description
am.CreateTime = result.CreateTime
am.UpdateTime = result.UpdateTime
am.Version = result.Version
am.OperationalState = result.State
am.StartTls = result.StartTLS
am.InsecureTls = result.InsecureTLS
am.DiscoverDn = result.DiscoverDn
am.AnonGroupSearch = result.AnonGroupSearch
am.EnableGroups = result.EnableGroups
am.UseTokenGroups = result.UseTokenGroups
am.UpnDomain = result.UpnDomain
if result.Urls != "" {
am.Urls = strings.Split(result.Urls, delimiter)
}
if result.Certs != "" {
am.Certificates = strings.Split(result.Certs, delimiter)
}
am.UserDn = result.UserDn
am.UserAttr = result.UserAttr
am.UserFilter = result.UserFilter
am.GroupDn = result.GroupDn
am.GroupAttr = result.GroupAttr
am.GroupFilter = result.GroupFilter
am.ClientCertificateKeyHmac = result.ClientCertificateKeyHmac
am.ClientCertificate = string(result.ClientCertificateCert)
am.BindDn = result.BindDn
if result.AccountAttributeMap != "" {
am.AccountAttributeMaps = strings.Split(result.AccountAttributeMap, delimiter)
}
am.DereferenceAliases = result.DereferenceAliases
am.MaximumPageSize = result.MaximumPageSize
return &am, nil
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oidc
import (
"context"
"strings"
"github.com/hashicorp/boundary/internal/auth"
)
func init() {
auth.RegisterAuthMethodSubtype("oidc", &authMethodHooks{})
}
type authMethodHooks struct{}
// NewAuthMethod creates a new oidc auth method from the result
func (authMethodHooks) NewAuthMethod(ctx context.Context, result *auth.AuthMethodListQueryResult) (auth.AuthMethod, error) {
delimiter := "|"
am := AllocAuthMethod()
am.PublicId = result.PublicId
am.ScopeId = result.ScopeId
am.IsPrimaryAuthMethod = result.IsPrimaryAuthMethod
am.Name = result.Name
am.Description = result.Description
am.CreateTime = result.CreateTime
am.UpdateTime = result.UpdateTime
am.Version = result.Version
am.OperationalState = result.State
am.DisableDiscoveredConfigValidation = result.DisableDiscoveredConfigValidation
am.Issuer = result.Issuer
am.ClientId = result.ClientId
am.ClientSecretHmac = result.ClientSecretHmac
am.KeyId = result.KeyId
am.MaxAge = int32(result.MaxAge)
am.ApiUrl = result.ApiUrl
if result.Algs != "" {
am.SigningAlgs = strings.Split(result.Algs, delimiter)
}
if result.Auds != "" {
am.AudClaims = strings.Split(result.Auds, delimiter)
}
if result.Certs != "" {
am.Certificates = strings.Split(result.Certs, delimiter)
}
if result.ClaimsScopes != "" {
am.ClaimsScopes = strings.Split(result.ClaimsScopes, delimiter)
}
if result.AccountClaimMaps != "" {
am.AccountClaimMaps = strings.Split(result.AccountClaimMaps, delimiter)
}
if result.Prompts != "" {
am.Prompts = strings.Split(result.Prompts, delimiter)
}
return &am, nil
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
"github.com/hashicorp/boundary/internal/pagination"
)
// The option type is how options are passed as arugments
type Option func(*Options) error
// Options is a struct that contains all the possible auth options
type Options struct {
WithLimit int
WithStartPageAfterItem pagination.Item
WithUnauthenticatedUser bool
}
// getDefaultOptions returns a new Options struct
func getDefaultOptions() *Options {
return &Options{}
}
// GetOpts loops over the options and provides
// the configured options struct
func GetOpts(opts ...Option) (*Options, error) {
newOpts := getDefaultOptions()
for _, opt := range opts {
if err := opt(newOpts); err != nil {
return nil, err
}
}
return newOpts, nil
}
// WithLimit provides an option to provide a limit.
// If WithLimit <= 0, then default limits are used for results.
func WithLimit(_ context.Context, l int) Option {
return func(o *Options) error {
o.WithLimit = l
return nil
}
}
// WithStartPageAfterItem is used to paginate over the results.
// The next page will start after the provided item.
func WithStartPageAfterItem(_ context.Context, item pagination.Item) Option {
return func(o *Options) error {
o.WithStartPageAfterItem = item
return nil
}
}
// WithUnauthenticatedUser provides an option for filtering results for
// an unauthenticated users.
func WithUnauthenticatedUser(_ context.Context, enabled bool) Option {
return func(o *Options) error {
o.WithUnauthenticatedUser = enabled
return nil
}
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
"testing"
"time"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeItem struct {
pagination.Item
publicId string
createTime time.Time
updateTime time.Time
}
func (p *fakeItem) GetPublicId() string {
return p.publicId
}
func (p *fakeItem) GetUpdateTime() *timestamp.Timestamp {
return timestamp.New(p.updateTime)
}
func (p *fakeItem) GetCreateTime() *timestamp.Timestamp {
return timestamp.New(p.createTime)
}
// Test_GetOpts provides unit tests for GetOpts and all the options
func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("WithLimit", func(t *testing.T) {
ctx := context.Background()
opts, err := GetOpts()
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.WithLimit = 0
assert.Equal(t, opts, testOpts)
opts, err = GetOpts(WithLimit(ctx, -1))
require.NoError(t, err)
testOpts = getDefaultOptions()
testOpts.WithLimit = -1
assert.Equal(t, opts, testOpts)
opts, err = GetOpts(WithLimit(ctx, 1))
require.NoError(t, err)
testOpts = getDefaultOptions()
testOpts.WithLimit = 1
assert.Equal(t, opts, testOpts)
})
t.Run("WithReaderWriter", func(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
_, err := GetOpts()
require.NoError(t, err)
})
})
t.Run("WithStartPageAfterItem", func(t *testing.T) {
assert := assert.New(t)
updateTime := time.Now()
createTime := time.Now()
opts, err := GetOpts(WithStartPageAfterItem(context.Background(), &fakeItem{nil, "s_1", createTime, updateTime}))
require.NoError(t, err)
assert.Equal(opts.WithStartPageAfterItem.GetPublicId(), "s_1")
assert.Equal(opts.WithStartPageAfterItem.GetUpdateTime(), timestamp.New(updateTime))
assert.Equal(opts.WithStartPageAfterItem.GetCreateTime(), timestamp.New(createTime))
})
t.Run("WithUnauthenticatedUser", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
opts, err := GetOpts(WithUnauthenticatedUser(ctx, true))
require.NoError(t, err)
assert.True(t, opts.WithUnauthenticatedUser)
})
}
File renamed without changes.
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package password
import (
"context"
"github.com/hashicorp/boundary/internal/auth"
)
func init() {
auth.RegisterAuthMethodSubtype("password", &authMethodHooks{})
}
type authMethodHooks struct{}
// NewAuthMethod creates a new static auth method from the result
func (authMethodHooks) NewAuthMethod(ctx context.Context, result *auth.AuthMethodListQueryResult) (auth.AuthMethod, error) {
am := allocAuthMethod()
am.PublicId = result.PublicId
am.ScopeId = result.ScopeId
am.IsPrimaryAuthMethod = result.IsPrimaryAuthMethod
am.CreateTime = result.CreateTime
am.UpdateTime = result.UpdateTime
am.Name = result.Name
am.Description = result.Description
am.Version = result.Version
am.PasswordConfId = result.PasswordConfId
am.MinLoginNameLength = result.MinLoginNameLength
am.MinPasswordLength = result.MinPasswordLength
return &am, nil
}
+808
View File
@@ -0,0 +1,808 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
const (
estimateCountAuthMethodsQuery = `
select sum(reltuples::bigint) as estimate from pg_class where oid in (
'auth_password_method'::regclass,
'auth_ldap_method'::regclass,
'auth_oidc_method'::regclass
)
`
listDeletedIdsQuery = `
select public_id
from auth_password_method_deleted
where delete_time >= @since
union
select public_id
from auth_oidc_method_deleted
where delete_time >= @since
union
select public_id
from auth_ldap_method_deleted
where delete_time >= @since
`
listAuthMethodsTemplate = `
with auth_methods as (
select public_id
from auth_method
where %s -- search condition for scope IDs is constructed
order by create_time desc, public_id desc
limit %d
),
ldap as (
select *
from ldap_auth_method_with_value_obj
where public_id in (select public_id from auth_methods)
),
oidc as (
select *
from oidc_auth_method_with_value_obj
where public_id in (select public_id from auth_methods)
),
password as (
select *
from auth_password_method_with_is_primary
where public_id in (select public_id from auth_methods)
),
final as (
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
state,
start_tls,
insecure_tls,
discover_dn,
anon_group_search,
upn_domain,
enable_groups,
use_token_groups,
maximum_page_size,
urls,
certs,
account_attribute_map,
user_dn,
user_attr,
user_filter,
group_dn,
group_attr,
group_filter,
client_certificate_key,
client_certificate_key_hmac,
client_certificate_key_id,
client_certificate_cert,
bind_dn,
bind_password,
bind_password_hmac,
bind_password_key_id,
dereference_aliases,
null as disable_discovered_config_validation, -- Add to make union uniform
null as api_url,
null as issuer,
null as client_id,
null as client_secret,
null as client_secret_hmac,
null as key_id,
null as max_age,
null as algs,
null as auds,
null as certs,
null as claims_scopes,
null as prompts,
null as account_claim_maps,
null as password_conf_id,
null::integer as min_login_name_length,
null::integer as min_password_length,
'ldap' as subtype
from ldap
union
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
state,
null as start_tls, -- Add to make union uniform
null as insecure_tls,
null as discover_dn,
null as anon_group_search,
null as upn_domain,
null as enable_groups,
null as use_token_groups,
null as maximum_page_size,
null as urls,
null as certs,
null as account_attribute_map,
null as user_dn,
null as user_attr,
null as user_filter,
null as group_dn,
null as group_attr,
null as group_filter,
null as client_certificate_key,
null as client_certificate_key_hmac,
null as client_certificate_key_id,
null as client_certificate_cert,
null as bind_dn,
null as bind_password,
null as bind_password_hmac,
null as bind_password_key_id,
null as dereference_aliases,
disable_discovered_config_validation,
api_url,
issuer,
client_id,
client_secret,
client_secret_hmac,
key_id,
max_age,
algs,
auds,
certs,
claims_scopes,
prompts,
account_claim_maps,
null as password_conf_id,
null::integer as min_login_name_length,
null::integer as min_password_length,
'oidc' as subtype
from oidc
union
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
null as state, -- Add to make union uniform
null as start_tls,
null as insecure_tls,
null as discover_dn,
null as anon_group_search,
null as upn_domain,
null as enable_groups,
null as use_token_groups,
null as maximum_page_size,
null as urls,
null as certs,
null as account_attribute_map,
null as user_dn,
null as user_attr,
null as user_filter,
null as group_dn,
null as group_attr,
null as group_filter,
null as client_certificate_key,
null as client_certificate_key_hmac,
null as client_certificate_key_id,
null as client_certificate_cert,
null as bind_dn,
null as bind_password,
null as bind_password_hmac,
null as bind_password_key_id,
null as dereference_aliases,
null as disable_discovered_config_validation,
null as api_url,
null as issuer,
null as client_id,
null as client_secret,
null as client_secret_hmac,
null as key_id,
null as max_age,
null as algs,
null as auds,
null as certs,
null as claims_scopes,
null as prompts,
null as account_claim_maps,
password_conf_id,
min_login_name_length,
min_password_length,
'password' as subtype
from password
)
select *
from final
order by create_time desc, public_id desc;
`
listAuthMethodsPageTemplate = `
with auth_methods as (
select public_id
from auth_method
where (create_time, public_id) < (@last_item_create_time, @last_item_id)
and %s -- search condition for scope IDs is constructed
order by create_time desc, public_id desc
limit %d
),
ldap as (
select *
from ldap_auth_method_with_value_obj
where public_id in (select public_id from auth_methods)
),
oidc as (
select *
from oidc_auth_method_with_value_obj
where public_id in (select public_id from auth_methods)
),
password as (
select *
from auth_password_method_with_is_primary
where public_id in (select public_id from auth_methods)
),
final as (
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
state,
start_tls,
insecure_tls,
discover_dn,
anon_group_search,
upn_domain,
enable_groups,
use_token_groups,
maximum_page_size,
urls,
certs,
account_attribute_map,
user_dn,
user_attr,
user_filter,
group_dn,
group_attr,
group_filter,
client_certificate_key,
client_certificate_key_hmac,
client_certificate_key_id,
client_certificate_cert,
bind_dn,
bind_password,
bind_password_hmac,
bind_password_key_id,
dereference_aliases,
null as disable_discovered_config_validation, -- Add to make union uniform
null as api_url,
null as issuer,
null as client_id,
null as client_secret,
null as client_secret_hmac,
null as key_id,
null as max_age,
null as algs,
null as auds,
null as certs,
null as claims_scopes,
null as prompts,
null as account_claim_maps,
null as password_conf_id,
null::integer as min_login_name_length,
null::integer as min_password_length,
'ldap' as subtype
from ldap
union
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
state,
null as start_tls, -- Add to make union uniform
null as insecure_tls,
null as discover_dn,
null as anon_group_search,
null as upn_domain,
null as enable_groups,
null as use_token_groups,
null as maximum_page_size,
null as urls,
null as certs,
null as account_attribute_map,
null as user_dn,
null as user_attr,
null as user_filter,
null as group_dn,
null as group_attr,
null as group_filter,
null as client_certificate_key,
null as client_certificate_key_hmac,
null as client_certificate_key_id,
null as client_certificate_cert,
null as bind_dn,
null as bind_password,
null as bind_password_hmac,
null as bind_password_key_id,
null as dereference_aliases,
disable_discovered_config_validation,
api_url,
issuer,
client_id,
client_secret,
client_secret_hmac,
key_id,
max_age,
algs,
auds,
certs,
claims_scopes,
prompts,
account_claim_maps,
null as password_conf_id,
null::integer as min_login_name_length,
null::integer as min_password_length,
'oidc' as subtype
from oidc
union
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
null as state, -- Add to make union uniform
null as start_tls,
null as insecure_tls,
null as discover_dn,
null as anon_group_search,
null as upn_domain,
null as enable_groups,
null as use_token_groups,
null as maximum_page_size,
null as urls,
null as certs,
null as account_attribute_map,
null as user_dn,
null as user_attr,
null as user_filter,
null as group_dn,
null as group_attr,
null as group_filter,
null as client_certificate_key,
null as client_certificate_key_hmac,
null as client_certificate_key_id,
null as client_certificate_cert,
null as bind_dn,
null as bind_password,
null as bind_password_hmac,
null as bind_password_key_id,
null as dereference_aliases,
null as disable_discovered_config_validation,
null as api_url,
null as issuer,
null as client_id,
null as client_secret,
null as client_secret_hmac,
null as key_id,
null as max_age,
null as algs,
null as auds,
null as certs,
null as claims_scopes,
null as prompts,
null as account_claim_maps,
password_conf_id,
min_login_name_length,
min_password_length,
'password' as subtype
from password
)
select *
from final
order by create_time desc, public_id desc;
`
listAuthMethodsRefreshTemplate = `
with auth_methods as (
select public_id
from auth_method
where update_time > @updated_after_time
and %s -- search condition for scope IDs is constructed
order by update_time desc, public_id desc
limit %d
),
ldap as (
select *
from ldap_auth_method_with_value_obj
where public_id in (select public_id from auth_methods)
),
oidc as (
select *
from oidc_auth_method_with_value_obj
where public_id in (select public_id from auth_methods)
),
password as (
select *
from auth_password_method_with_is_primary
where public_id in (select public_id from auth_methods)
),
final as (
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
state,
start_tls,
insecure_tls,
discover_dn,
anon_group_search,
upn_domain,
enable_groups,
use_token_groups,
maximum_page_size,
urls,
certs,
account_attribute_map,
user_dn,
user_attr,
user_filter,
group_dn,
group_attr,
group_filter,
client_certificate_key,
client_certificate_key_hmac,
client_certificate_key_id,
client_certificate_cert,
bind_dn,
bind_password,
bind_password_hmac,
bind_password_key_id,
dereference_aliases,
null as disable_discovered_config_validation, -- Add to make union uniform
null as api_url,
null as issuer,
null as client_id,
null as client_secret,
null as client_secret_hmac,
null as key_id,
null as max_age,
null as algs,
null as auds,
null as certs,
null as claims_scopes,
null as prompts,
null as account_claim_maps,
null as password_conf_id,
null::integer as min_login_name_length,
null::integer as min_password_length,
'ldap' as subtype
from ldap
union
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
state,
null as start_tls, -- Add to make union uniform
null as insecure_tls,
null as discover_dn,
null as anon_group_search,
null as upn_domain,
null as enable_groups,
null as use_token_groups,
null as maximum_page_size,
null as urls,
null as certs,
null as account_attribute_map,
null as user_dn,
null as user_attr,
null as user_filter,
null as group_dn,
null as group_attr,
null as group_filter,
null as client_certificate_key,
null as client_certificate_key_hmac,
null as client_certificate_key_id,
null as client_certificate_cert,
null as bind_dn,
null as bind_password,
null as bind_password_hmac,
null as bind_password_key_id,
null as dereference_aliases,
disable_discovered_config_validation,
api_url,
issuer,
client_id,
client_secret,
client_secret_hmac,
key_id,
max_age,
algs,
auds,
certs,
claims_scopes,
prompts,
account_claim_maps,
null as password_conf_id,
null::integer as min_login_name_length,
null::integer as min_password_length,
'oidc' as subtype
from oidc
union
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
null as state, -- Add to make union uniform
null as start_tls,
null as insecure_tls,
null as discover_dn,
null as anon_group_search,
null as upn_domain,
null as enable_groups,
null as use_token_groups,
null as maximum_page_size,
null as urls,
null as certs,
null as account_attribute_map,
null as user_dn,
null as user_attr,
null as user_filter,
null as group_dn,
null as group_attr,
null as group_filter,
null as client_certificate_key,
null as client_certificate_key_hmac,
null as client_certificate_key_id,
null as client_certificate_cert,
null as bind_dn,
null as bind_password,
null as bind_password_hmac,
null as bind_password_key_id,
null as dereference_aliases,
null as disable_discovered_config_validation,
null as api_url,
null as issuer,
null as client_id,
null as client_secret,
null as client_secret_hmac,
null as key_id,
null as max_age,
null as algs,
null as auds,
null as certs,
null as claims_scopes,
null as prompts,
null as account_claim_maps,
password_conf_id,
min_login_name_length,
min_password_length,
'password' as subtype
from password
)
select *
from final
order by update_time desc, public_id desc;
`
listAuthMethodsRefreshPageTemplate = `
with auth_methods as (
select public_id
from auth_method
where update_time > @updated_after_time
and (update_time, public_id) < (@last_item_update_time, @last_item_id)
and %s -- search condition for scope IDs is constructed
order by update_time desc, public_id desc
limit %d
),
ldap as (
select *
from ldap_auth_method_with_value_obj
where public_id in (select public_id from auth_methods)
),
oidc as (
select *
from oidc_auth_method_with_value_obj
where public_id in (select public_id from auth_methods)
),
password as (
select *
from auth_password_method_with_is_primary
where public_id in (select public_id from auth_methods)
),
final as (
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
state,
start_tls,
insecure_tls,
discover_dn,
anon_group_search,
upn_domain,
enable_groups,
use_token_groups,
maximum_page_size,
urls,
certs,
account_attribute_map,
user_dn,
user_attr,
user_filter,
group_dn,
group_attr,
group_filter,
client_certificate_key,
client_certificate_key_hmac,
client_certificate_key_id,
client_certificate_cert,
bind_dn,
bind_password,
bind_password_hmac,
bind_password_key_id,
dereference_aliases,
null as disable_discovered_config_validation, -- Add to make union uniform
null as api_url,
null as issuer,
null as client_id,
null as client_secret,
null as client_secret_hmac,
null as key_id,
null as max_age,
null as algs,
null as auds,
null as certs,
null as claims_scopes,
null as prompts,
null as account_claim_maps,
null as password_conf_id,
null::integer as min_login_name_length,
null::integer as min_password_length,
'ldap' as subtype
from ldap
union
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
state,
null as start_tls, -- Add to make union uniform
null as insecure_tls,
null as discover_dn,
null as anon_group_search,
null as upn_domain,
null as enable_groups,
null as use_token_groups,
null as maximum_page_size,
null as urls,
null as certs,
null as account_attribute_map,
null as user_dn,
null as user_attr,
null as user_filter,
null as group_dn,
null as group_attr,
null as group_filter,
null as client_certificate_key,
null as client_certificate_key_hmac,
null as client_certificate_key_id,
null as client_certificate_cert,
null as bind_dn,
null as bind_password,
null as bind_password_hmac,
null as bind_password_key_id,
null as dereference_aliases,
disable_discovered_config_validation,
api_url,
issuer,
client_id,
client_secret,
client_secret_hmac,
key_id,
max_age,
algs,
auds,
certs,
claims_scopes,
prompts,
account_claim_maps,
null as password_conf_id,
null::integer as min_login_name_length,
null::integer as min_password_length,
'oidc' as subtype
from oidc
union
select public_id,
scope_id,
is_primary_auth_method,
name,
description,
create_time,
update_time,
version,
null as state, -- Add to make union uniform
null as start_tls,
null as insecure_tls,
null as discover_dn,
null as anon_group_search,
null as upn_domain,
null as enable_groups,
null as use_token_groups,
null as maximum_page_size,
null as urls,
null as certs,
null as account_attribute_map,
null as user_dn,
null as user_attr,
null as user_filter,
null as group_dn,
null as group_attr,
null as group_filter,
null as client_certificate_key,
null as client_certificate_key_hmac,
null as client_certificate_key_id,
null as client_certificate_cert,
null as bind_dn,
null as bind_password,
null as bind_password_hmac,
null as bind_password_key_id,
null as dereference_aliases,
null as disable_discovered_config_validation,
null as api_url,
null as issuer,
null as client_id,
null as client_secret,
null as client_secret_hmac,
null as key_id,
null as max_age,
null as algs,
null as auds,
null as certs,
null as claims_scopes,
null as prompts,
null as account_claim_maps,
password_conf_id,
min_login_name_length,
min_password_length,
'password' as subtype
from password
)
select *
from final
order by update_time desc, public_id desc;
`
)
+68
View File
@@ -0,0 +1,68 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/boundary/globals"
)
// newAuthMethodFunc is a function that creates an AuthMethod from the provided result.
type newAuthMethodFunc func(context.Context, *AuthMethodListQueryResult) (AuthMethod, error)
// AuthMethodSubtypeHooks defines the interface expected to be implemented
// by auth method subtype hooks.
type AuthMethodSubtypeHooks interface {
NewAuthMethod(context.Context, *AuthMethodListQueryResult) (AuthMethod, error)
}
type registry struct {
m map[globals.Subtype]AuthMethodSubtypeHooks
sync.RWMutex
}
func (r *registry) set(s globals.Subtype, subtype AuthMethodSubtypeHooks) {
r.Lock()
defer r.Unlock()
_, previouslySet := r.m[s]
if previouslySet {
panic(fmt.Sprintf("auth method subtype %s already registered", s))
}
r.m[s] = subtype
}
func (r *registry) get(s globals.Subtype) (AuthMethodSubtypeHooks, 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) (newAuthMethodFunc, bool) {
subtype, ok := r.get(s)
if !ok {
return nil, ok
}
return subtype.NewAuthMethod, ok
}
var subtypeRegistry = registry{
m: make(map[globals.Subtype]AuthMethodSubtypeHooks),
}
// RegisterAuthMethodSubtype registers repository hooks for a provided auth method sub type.
// RegisterAuthMethodSubtype panics if the subtype has already been registered.
func RegisterAuthMethodSubtype(s globals.Subtype, hooks AuthMethodSubtypeHooks) {
subtypeRegistry.set(s, hooks)
}
+197
View File
@@ -0,0 +1,197 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
"database/sql"
"fmt"
"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/kms"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/util"
)
// AuthMethodRepository coordinates calls across different subtype services
// to gather information about all auth methods.
type AuthMethodRepository struct {
reader db.Reader
writer db.Writer
kms *kms.Kms
}
// NewAuthMethodRepository returns a new auth method repository.
func NewAuthMethodRepository(ctx context.Context, reader db.Reader, writer db.Writer, kms *kms.Kms) (*AuthMethodRepository, error) {
const op = "auth.NewAuthMethodRepository"
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")
case util.IsNil(kms):
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing kms")
}
return &AuthMethodRepository{
reader: reader,
writer: writer,
kms: kms,
}, nil
}
// List lists auth methods across all subtypes.
func (amr *AuthMethodRepository) List(ctx context.Context, scopeIds []string, afterItem pagination.Item, opt ...Option) ([]AuthMethod, time.Time, error) {
const op = "auth.(*AuthMethodRepository).list"
opts, err := GetOpts(opt...)
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
limit := opts.WithLimit
switch {
case len(scopeIds) == 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing scope ids")
case limit < 1:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing limit")
}
whereClause := "scope_id in @scope_ids"
args := []any{sql.Named("scope_ids", scopeIds)}
if opts.WithUnauthenticatedUser {
whereClause += " and is_active_public_state = true"
}
query := fmt.Sprintf(listAuthMethodsTemplate, whereClause, limit)
if afterItem != nil {
query = fmt.Sprintf(listAuthMethodsPageTemplate, whereClause, limit)
args = append(args,
sql.Named("last_item_create_time", afterItem.GetCreateTime()),
sql.Named("last_item_id", afterItem.GetPublicId()),
)
}
return amr.queryAuthMethods(ctx, query, args)
}
// ListRefresh lists auth methods across all subtypes.
func (amr *AuthMethodRepository) ListRefresh(ctx context.Context, scopeIds []string, updatedAfter time.Time, afterItem pagination.Item, opt ...Option) ([]AuthMethod, time.Time, error) {
const op = "auth.(*AuthMethodRepository).list"
opts, err := GetOpts(opt...)
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
limit := opts.WithLimit
switch {
case len(scopeIds) == 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing scope 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")
}
whereClause := "scope_id in @scope_ids"
args := []any{sql.Named("scope_ids", scopeIds)}
if opts.WithUnauthenticatedUser {
whereClause += " and is_active_public_state = true"
}
query := fmt.Sprintf(listAuthMethodsRefreshTemplate, whereClause, limit)
args = append(args,
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
)
if afterItem != nil {
query = fmt.Sprintf(listAuthMethodsRefreshPageTemplate, whereClause, limit)
args = append(args,
sql.Named("last_item_update_time", afterItem.GetUpdateTime()),
sql.Named("last_item_id", afterItem.GetPublicId()),
)
}
return amr.queryAuthMethods(ctx, query, args)
}
// EstimatedCount estimates the total number of auth methods.
func (amr *AuthMethodRepository) EstimatedCount(ctx context.Context) (int, error) {
const op = "auth.(*AuthMethodRepository).EstimatedCount"
rows, err := amr.reader.Query(ctx, estimateCountAuthMethodsQuery, nil)
if err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total auth methods"))
}
var count int
for rows.Next() {
if err := amr.reader.ScanRows(ctx, rows, &count); err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total auth methods"))
}
}
return count, nil
}
// ListDeletedIds lists the deleted IDs of all auth methods since the time specified.
func (amr *AuthMethodRepository) ListDeletedIds(ctx context.Context, since time.Time) ([]string, time.Time, error) {
const op = "auth.(*AuthMethodRepository).ListDeletedIds"
var deletedAuthMethodIDs []string
var transactionTimestamp time.Time
if _, err := amr.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
rows, err := amr.writer.Query(ctx, listDeletedIdsQuery, []any{sql.Named("since", since)})
if err != nil {
return errors.Wrap(ctx, err, op)
}
defer rows.Close()
for rows.Next() {
if err := r.ScanRows(ctx, rows, &deletedAuthMethodIDs); err != nil {
return errors.Wrap(ctx, err, op)
}
}
transactionTimestamp, err = r.Now(ctx)
return err
}); err != nil {
return nil, time.Time{}, err
}
return deletedAuthMethodIDs, transactionTimestamp, nil
}
func (amr *AuthMethodRepository) queryAuthMethods(ctx context.Context, query string, args []any) ([]AuthMethod, time.Time, error) {
const op = "auth.(*AuthMethodRepository).queryAuthMethods"
var authmethods []AuthMethod
var transactionTimestamp time.Time
if _, err := amr.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 foundAuthMethods []*AuthMethodListQueryResult
for rows.Next() {
if err := r.ScanRows(ctx, rows, &foundAuthMethods); err != nil {
return err
}
}
for _, am := range foundAuthMethods {
authmethod, err := am.toAuthMethod(ctx)
if err != nil {
return err
}
authmethods = append(authmethods, authmethod)
}
transactionTimestamp, err = r.Now(ctx)
return err
}); err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
return authmethods, transactionTimestamp, nil
}
@@ -0,0 +1,390 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth_test
import (
"context"
"slices"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/ldap"
lstore "github.com/hashicorp/boundary/internal/auth/ldap/store"
"github.com/hashicorp/boundary/internal/auth/oidc"
ostore "github.com/hashicorp/boundary/internal/auth/oidc/store"
"github.com/hashicorp/boundary/internal/auth/password"
pstore "github.com/hashicorp/boundary/internal/auth/password/store"
"github.com/hashicorp/boundary/internal/auth/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 TestAuthMethodRepository_List(t *testing.T) {
t.Parallel()
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, conn, wrapper)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
ams := []auth.AuthMethod{
// two ldap
ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState)),
ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState)),
// two oidc
oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "alice_rp", "alices-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])),
oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "bob_rp", "bobs-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://bob-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])),
// two password
password.TestAuthMethod(t, conn, org.GetPublicId()),
password.TestAuthMethod(t, conn, org.GetPublicId()),
}
// since we sort descending, we need to reverse the slice
slices.Reverse(ams)
repo, err := auth.NewAuthMethodRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
ldap.AuthMethod{},
lstore.AuthMethod{},
oidc.AuthMethod{},
ostore.AuthMethod{},
password.AuthMethod{},
pstore.AuthMethod{},
store.AuthMethod{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
cmpopts.IgnoreFields(
oidc.AuthMethod{}, "CtClientSecret", "ClientSecret",
),
}
t.Run("validation", func(t *testing.T) {
t.Parallel()
t.Run("missing scope ids", func(t *testing.T) {
t.Parallel()
_, _, err := repo.List(ctx, nil, nil, auth.WithLimit(ctx, 1))
require.ErrorContains(t, err, "missing scope ids")
})
t.Run("invalid limit", func(t *testing.T) {
t.Parallel()
_, _, err := repo.List(ctx, []string{org.PublicId}, nil, auth.WithLimit(ctx, 0))
require.ErrorContains(t, err, "missing limit")
})
})
t.Run("success-without-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.List(ctx, []string{org.PublicId}, nil, auth.WithLimit(ctx, 10))
require.NoError(t, err)
// 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)))
require.Empty(t, cmp.Diff(resp, ams, cmpOpts...))
})
t.Run("success-with-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.List(ctx, []string{org.PublicId}, ams[0], auth.WithLimit(ctx, 10))
require.NoError(t, err)
// 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)))
require.Empty(t, cmp.Diff(resp, ams[1:], cmpOpts...))
})
t.Run("success-with-unauthenticated-user", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.List(ctx, []string{org.PublicId}, nil, auth.WithLimit(ctx, 10), auth.WithUnauthenticatedUser(ctx, true))
require.NoError(t, err)
// 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)))
require.Empty(t, cmp.Diff(resp, ams[0:2], cmpOpts...))
})
}
func TestAuthMethodRepository_ListRefresh(t *testing.T) {
t.Parallel()
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, conn, wrapper)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
ams := []auth.AuthMethod{
// two ldap
ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState)),
ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState)),
// two oidc
oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "alice_rp", "alices-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])),
oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "bob_rp", "bobs-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://bob-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])),
// two password
password.TestAuthMethod(t, conn, org.GetPublicId()),
password.TestAuthMethod(t, conn, org.GetPublicId()),
}
// since we sort descending, we need to reverse the slice
slices.Reverse(ams)
repo, err := auth.NewAuthMethodRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
ldap.AuthMethod{},
lstore.AuthMethod{},
oidc.AuthMethod{},
ostore.AuthMethod{},
password.AuthMethod{},
pstore.AuthMethod{},
store.AuthMethod{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
cmpopts.IgnoreFields(
oidc.AuthMethod{}, "CtClientSecret", "ClientSecret",
),
}
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{org.PublicId}, time.Time{}, nil, auth.WithLimit(ctx, 1))
require.ErrorContains(t, err, "missing updated after time")
})
t.Run("missing scope ids", func(t *testing.T) {
t.Parallel()
_, _, err := repo.ListRefresh(ctx, nil, fiveDaysAgo, nil, auth.WithLimit(ctx, 1))
require.ErrorContains(t, err, "missing scope ids")
})
t.Run("invalid limit", func(t *testing.T) {
t.Parallel()
_, _, err := repo.ListRefresh(ctx, []string{org.PublicId}, fiveDaysAgo, nil, auth.WithLimit(ctx, 0))
require.ErrorContains(t, err, "missing limit")
})
})
t.Run("success-without-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListRefresh(ctx, []string{org.PublicId}, fiveDaysAgo, nil, auth.WithLimit(ctx, 10))
require.NoError(t, err)
// 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)))
require.Empty(t, cmp.Diff(resp, ams, cmpOpts...))
})
t.Run("success-with-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListRefresh(ctx, []string{org.PublicId}, fiveDaysAgo, ams[0], auth.WithLimit(ctx, 10))
require.NoError(t, err)
// 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)))
require.Empty(t, cmp.Diff(resp, ams[1:], cmpOpts...))
})
t.Run("success-without-after-item-recent-updated-after", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListRefresh(ctx, []string{org.PublicId}, ams[len(ams)-1].GetUpdateTime().AsTime(), nil, auth.WithLimit(ctx, 10))
require.NoError(t, err)
// 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)))
require.Empty(t, cmp.Diff(resp, ams[:len(ams)-1], cmpOpts...))
})
t.Run("success-with-after-item-recent-updated-after", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListRefresh(ctx, []string{org.PublicId}, ams[len(ams)-1].GetUpdateTime().AsTime(), ams[0], auth.WithLimit(ctx, 10))
require.NoError(t, err)
// 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)))
require.Empty(t, cmp.Diff(resp, ams[1:len(ams)-1], cmpOpts...))
})
}
func TestAuthMethodRepository_EstimatedCount(t *testing.T) {
t.Parallel()
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, conn, wrapper)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
ldapRepo, err := ldap.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
oidcRepo, err := oidc.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
passwordRepo, err := password.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
repo, err := auth.NewAuthMethodRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
// Check total entries at start, expect 0
numItems, err := repo.EstimatedCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
// Create auth methods
am1 := ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState))
am2 := oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "alice_rp", "alices-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]))
am3 := password.TestAuthMethod(t, conn, org.GetPublicId())
// Run analyze to update postgres meta tables
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
numItems, err = repo.EstimatedCount(ctx)
require.NoError(t, err)
assert.Equal(t, 3, numItems)
// Delete ldap auth method
_, err = ldapRepo.DeleteAuthMethod(ctx, am1.GetPublicId())
require.NoError(t, err)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
numItems, err = repo.EstimatedCount(ctx)
require.NoError(t, err)
assert.Equal(t, 2, numItems)
// Delete oidc auth method
_, err = oidcRepo.DeleteAuthMethod(ctx, am2.GetPublicId())
require.NoError(t, err)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
numItems, err = repo.EstimatedCount(ctx)
require.NoError(t, err)
assert.Equal(t, 1, numItems)
// Delete pw auth method
_, err = passwordRepo.DeleteAuthMethod(ctx, org.GetPublicId(), am3.GetPublicId())
require.NoError(t, err)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
numItems, err = repo.EstimatedCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
}
func TestRepository_ListDeletedStoreIds(t *testing.T) {
t.Parallel()
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, conn, wrapper)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
ldapRepo, err := ldap.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
oidcRepo, err := oidc.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
passwordRepo, err := password.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
repo, err := auth.NewAuthMethodRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
// Check total entries at start, expect 0
numItems, err := repo.EstimatedCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
// Create auth methods
am1 := ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState))
am2 := oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "alice_rp", "alices-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]))
am3 := password.TestAuthMethod(t, conn, org.GetPublicId())
// Expect no entries at the start
deletedIds, ttime, err := repo.ListDeletedIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(t, err)
require.Empty(t, 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)))
_, err = ldapRepo.DeleteAuthMethod(ctx, am1.GetPublicId())
require.NoError(t, err)
// Expect one entry
deletedIds, ttime, err = repo.ListDeletedIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(t, err)
assert.Empty(
t,
cmp.Diff(
[]string{am1.GetPublicId()},
deletedIds,
cmpopts.SortSlices(func(i, j string) bool { return i < j }),
),
)
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.ListDeletedIds(ctx, time.Now())
require.NoError(t, err)
require.Empty(t, deletedIds)
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
_, err = oidcRepo.DeleteAuthMethod(ctx, am2.GetPublicId())
require.NoError(t, err)
_, err = passwordRepo.DeleteAuthMethod(ctx, org.GetPublicId(), am3.GetPublicId())
require.NoError(t, err)
// Expect three entries (with a buffer of -30 seconds, we'll pick up am1)
deletedIds, ttime, err = repo.ListDeletedIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(t, err)
assert.Empty(
t,
cmp.Diff(
[]string{am1.GetPublicId(), am2.GetPublicId(), am3.GetPublicId()},
deletedIds,
cmpopts.SortSlices(func(i, j string) bool { return i < j }),
),
)
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.ListDeletedIds(ctx, time.Now())
require.NoError(t, err)
require.Empty(t, deletedIds)
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
}
@@ -0,0 +1,54 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
"time"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/pagination"
)
// ListAuthMethods lists up to page size auth methods, filtering out entries that
// do not pass the filter item function. It will automatically request
// more auth methods from the database, at page size chunks, to fill the page.
// It returns a new list token used to continue pagination or refresh items.
// Auth methods are ordered by create time descending (most recently created first).
func ListAuthMethods(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[AuthMethod],
repo *AuthMethodRepository,
scopeIds []string,
withUnauthenticatedUser bool,
) (*pagination.ListResponse[AuthMethod], error) {
const op = "auth.ListAuthMethods"
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 repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case len(scopeIds) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope ids")
}
listItemsFn := func(ctx context.Context, lastPageItem AuthMethod, limit int) ([]AuthMethod, time.Time, error) {
opts := []Option{
WithLimit(ctx, limit),
}
if withUnauthenticatedUser {
opts = append(opts, WithUnauthenticatedUser(ctx, true))
}
return repo.List(ctx, scopeIds, lastPageItem, opts...)
}
return pagination.List(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.EstimatedCount)
}
@@ -0,0 +1,711 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth_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/auth"
"github.com/hashicorp/boundary/internal/auth/ldap"
lstore "github.com/hashicorp/boundary/internal/auth/ldap/store"
"github.com/hashicorp/boundary/internal/auth/oidc"
ostore "github.com/hashicorp/boundary/internal/auth/oidc/store"
"github.com/hashicorp/boundary/internal/auth/password"
pstore "github.com/hashicorp/boundary/internal/auth/password/store"
"github.com/hashicorp/boundary/internal/auth/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/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
type fakeReader struct {
db.Reader
}
type fakeWriter struct {
db.Writer
}
func TestNewAuthMethodService(t *testing.T) {
t.Parallel()
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, conn, wrapper)
t.Run("success", func(t *testing.T) {
t.Parallel()
got, err := auth.NewAuthMethodRepository(ctx, &fakeReader{}, &fakeWriter{}, testKms)
require.NoError(t, err)
require.NotNil(t, got)
})
t.Run("nil-reader", func(t *testing.T) {
t.Parallel()
_, err := auth.NewAuthMethodRepository(ctx, nil, &fakeWriter{}, testKms)
require.Error(t, err)
})
t.Run("nil-interface-reader", func(t *testing.T) {
t.Parallel()
_, err := auth.NewAuthMethodRepository(ctx, (*fakeReader)(nil), &fakeWriter{}, testKms)
require.Error(t, err)
})
t.Run("nil-writer", func(t *testing.T) {
t.Parallel()
_, err := auth.NewAuthMethodRepository(ctx, &fakeReader{}, nil, testKms)
require.Error(t, err)
})
t.Run("nil-interface-writer", func(t *testing.T) {
t.Parallel()
_, err := auth.NewAuthMethodRepository(ctx, &fakeReader{}, (*fakeWriter)(nil), testKms)
require.Error(t, err)
})
t.Run("nil-kms", func(t *testing.T) {
t.Parallel()
_, err := auth.NewAuthMethodRepository(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")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
testKms := kms.TestKms(t, conn, wrapper)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
ldapRepo, err := ldap.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
oidcRepo, err := oidc.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
passwordRepo, err := password.NewRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
ams := []auth.AuthMethod{
// two ldap
ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState)),
ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState)),
// two oidc
oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "alice_rp", "alices-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])),
oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "bob_rp", "bobs-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://bob-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])),
// two password
password.TestAuthMethod(t, conn, org.GetPublicId()),
password.TestAuthMethod(t, conn, org.GetPublicId()),
}
// since we sort descending, we need to reverse the slice
slices.Reverse(ams)
repo, err := auth.NewAuthMethodRepository(ctx, rw, rw, testKms)
require.NoError(t, err)
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
ldap.AuthMethod{},
lstore.AuthMethod{},
oidc.AuthMethod{},
ostore.AuthMethod{},
password.AuthMethod{},
pstore.AuthMethod{},
store.AuthMethod{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
cmpopts.IgnoreFields(
oidc.AuthMethod{}, "CtClientSecret", "ClientSecret",
),
}
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
_, err := auth.ListAuthMethods(ctx, nil, 1, filterFunc, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
_, err := auth.ListAuthMethods(ctx, []byte("some hash"), 0, filterFunc, repo, []string{org.PublicId}, false)
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
_, err := auth.ListAuthMethods(ctx, []byte("some hash"), -1, filterFunc, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
_, err := auth.ListAuthMethods(ctx, []byte("some hash"), 1, nil, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("missing repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
_, err := auth.ListAuthMethods(ctx, []byte("some hash"), 1, filterFunc, nil, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing scope ids", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
_, err := auth.ListAuthMethods(ctx, []byte("some hash"), 1, filterFunc, repo, nil, false)
require.ErrorContains(t, err, "missing scope 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, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsPage(ctx, nil, 1, filterFunc, tok, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, []string{org.PublicId}, false)
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, []string{org.PublicId}, false)
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.AuthMethod, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, nil, tok, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
_, err = auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing token")
})
t.Run("wrong token type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, []string{org.PublicId}, false)
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing scope ids", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil, false)
require.ErrorContains(t, err, "missing scope ids")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, []string{org.GetPublicId()}, false)
require.ErrorContains(t, err, "token did not have an auth method resource type")
})
})
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefresh(ctx, nil, 1, filterFunc, tok, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 0, filterFunc, tok, repo, []string{org.PublicId}, false)
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), -1, filterFunc, tok, repo, []string{org.PublicId}, false)
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.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, nil, tok, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
_, err = auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, nil, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing token")
})
t.Run("missing repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, nil, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing scope ids", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil, false)
require.ErrorContains(t, err, "missing scope ids")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, []string{org.GetPublicId()}, false)
require.ErrorContains(t, err, "token did not have an auth method resource type")
})
})
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefreshPage(ctx, nil, 1, filterFunc, tok, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, []string{org.PublicId}, false)
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, []string{org.PublicId}, false)
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.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, nil, tok, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
_, err = auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing token")
})
t.Run("wrong token type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, []string{org.PublicId}, false)
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, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, []string{org.PublicId}, false)
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing scope ids", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.AuthMethod, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil, false)
require.ErrorContains(t, err, "missing scope ids")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, []string{org.GetPublicId()}, false)
require.ErrorContains(t, err, "token did not have an auth method resource type")
})
})
t.Run("simple pagination", func(t *testing.T) {
filterFunc := func(context.Context, auth.AuthMethod) (bool, error) {
return true, nil
}
resp, err := auth.ListAuthMethods(ctx, []byte("some hash"), 1, filterFunc, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.NotNil(t, resp.ListToken)
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp.CompleteListing)
require.Len(t, resp.Items, 1)
require.Equal(t, resp.EstimatedItemCount, 6)
require.Empty(t, resp.DeletedIds)
require.Empty(t, cmp.Diff(resp.Items[0], ams[0], cmpOpts...))
resp2, err := auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp2.CompleteListing)
require.Equal(t, resp2.EstimatedItemCount, 6)
require.Empty(t, resp2.DeletedIds)
require.Len(t, resp2.Items, 1)
require.Empty(t, cmp.Diff(resp2.Items[0], ams[1], cmpOpts...))
resp3, err := auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 6)
require.Empty(t, resp3.DeletedIds)
require.Len(t, resp3.Items, 1)
require.Empty(t, cmp.Diff(resp3.Items[0], ams[2], cmpOpts...))
resp4, err := auth.ListAuthMethodsPage(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp4.CompleteListing)
require.Equal(t, resp4.EstimatedItemCount, 6)
require.Empty(t, resp4.DeletedIds)
require.Len(t, resp4.Items, 1)
require.Empty(t, cmp.Diff(resp4.Items[0], ams[3], cmpOpts...))
// get the rest
resp5, err := auth.ListAuthMethodsPage(ctx, []byte("some hash"), 2, filterFunc, resp4.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp5.CompleteListing)
require.Equal(t, resp5.EstimatedItemCount, 6)
require.Empty(t, resp5.DeletedIds)
require.Len(t, resp5.Items, 2)
require.Empty(t, cmp.Diff(resp5.Items, ams[4:], cmpOpts...))
// Finished initial pagination phase, request refresh
// Expect no results.
resp6, err := auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp5.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp6.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp6.CompleteListing)
require.Equal(t, resp6.EstimatedItemCount, 6)
require.Empty(t, resp6.DeletedIds)
require.Empty(t, resp6.Items)
// Create some new auth methods
am1 := ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState))
am2 := oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "charlene_rp", "charlenes-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://charlene-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]))
am3 := password.TestAuthMethod(t, conn, org.GetPublicId())
t.Cleanup(func() {
_, err := ldapRepo.DeleteAuthMethod(ctx, am1.PublicId)
require.NoError(t, err)
_, err = oidcRepo.DeleteAuthMethod(ctx, am2.PublicId)
require.NoError(t, err)
_, err = passwordRepo.DeleteAuthMethod(ctx, org.PublicId, am3.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 am3
resp7, err := auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp6.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp7.CompleteListing)
require.Equal(t, resp7.EstimatedItemCount, 9)
require.Empty(t, resp7.DeletedIds)
require.Len(t, resp7.Items, 1)
require.Empty(t, cmp.Diff(resp7.Items[0], am3, cmpOpts...))
// Refresh again, should get am2
resp8, err := auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp7.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp8.CompleteListing)
require.Equal(t, resp8.EstimatedItemCount, 9)
require.Empty(t, resp8.DeletedIds)
require.Len(t, resp8.Items, 1)
require.Empty(t, cmp.Diff(resp8.Items[0], am2, cmpOpts...))
// Refresh again, should get am1
resp9, err := auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp8.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp9.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp9.CompleteListing)
require.Equal(t, resp9.EstimatedItemCount, 9)
require.Empty(t, resp9.DeletedIds)
require.Len(t, resp9.Items, 1)
require.Empty(t, cmp.Diff(resp9.Items[0], am1, cmpOpts...))
// Refresh again, should get no results
resp10, err := auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp9.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp10.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp10.CompleteListing)
require.Equal(t, resp10.EstimatedItemCount, 9)
require.Empty(t, resp10.DeletedIds)
require.Empty(t, resp10.Items)
})
t.Run("simple pagination with aggressive filtering", func(t *testing.T) {
filterFunc := func(ctx context.Context, am auth.AuthMethod) (bool, error) {
return am.GetPublicId() == ams[1].GetPublicId() ||
am.GetPublicId() == ams[len(ams)-1].GetPublicId(), nil
}
resp, err := auth.ListAuthMethods(ctx, []byte("some hash"), 1, filterFunc, repo, []string{org.PublicId}, false)
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, 6)
require.Empty(t, resp.DeletedIds)
require.Len(t, resp.Items, 1)
require.Empty(t, cmp.Diff(resp.Items[0], ams[1], cmpOpts...))
resp2, err := auth.ListAuthMethodsPage(ctx, []byte("some hash"), 2, filterFunc, resp.ListToken, repo, []string{org.PublicId}, false)
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, 6)
require.Empty(t, resp2.DeletedIds)
require.Len(t, resp2.Items, 1)
require.Empty(t, cmp.Diff(resp2.Items[0], ams[len(ams)-1], cmpOpts...))
// request a refresh, nothing should be returned
resp3, err := auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 6)
require.Empty(t, resp3.DeletedIds)
require.Empty(t, resp3.Items)
// Create some new auth methods
am1 := ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, ldap.WithOperationalState(ctx, ldap.InactiveState))
am2 := oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.InactiveState, "charlene_rp", "charlenes-dogs-name",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://charlene-inactive.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]))
am3 := password.TestAuthMethod(t, conn, org.GetPublicId())
t.Cleanup(func() {
_, err := ldapRepo.DeleteAuthMethod(ctx, am1.PublicId)
require.NoError(t, err)
_, err = oidcRepo.DeleteAuthMethod(ctx, am2.PublicId)
require.NoError(t, err)
_, err = passwordRepo.DeleteAuthMethod(ctx, org.PublicId, am3.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, am auth.AuthMethod) (bool, error) {
return am.GetPublicId() == am1.GetPublicId() ||
am.GetPublicId() == am3.GetPublicId(), nil
}
// Refresh again, should get am3
resp4, err := auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, []string{org.PublicId}, false)
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], am3, cmpOpts...))
// Refresh again, should get am1
resp5, err := auth.ListAuthMethodsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, []string{org.PublicId}, false)
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], am1, cmpOpts...))
})
t.Run("simple pagination with deletion", func(t *testing.T) {
filterFunc := func(ctx context.Context, am auth.AuthMethod) (bool, error) {
return true, nil
}
// delete passwords
deletedAmId1 := ams[0].GetPublicId()
_, err := passwordRepo.DeleteAuthMethod(ctx, org.GetPublicId(), deletedAmId1)
require.NoError(t, err)
ams = ams[1:]
deletedAmId2 := ams[0].GetPublicId()
_, err = passwordRepo.DeleteAuthMethod(ctx, org.GetPublicId(), deletedAmId2)
require.NoError(t, err)
ams = ams[1:]
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
resp, err := auth.ListAuthMethods(ctx, []byte("some hash"), 1, filterFunc, repo, []string{org.PublicId}, false)
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], ams[0], cmpOpts...))
// request remaining results
resp2, err := auth.ListAuthMethodsPage(ctx, []byte("some hash"), 3, filterFunc, resp.ListToken, repo, []string{org.PublicId}, false)
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, ams[1:], cmpOpts...))
// delete oidcs
deletedAmId3 := ams[0].GetPublicId()
_, err = oidcRepo.DeleteAuthMethod(ctx, deletedAmId3)
require.NoError(t, err)
ams = ams[1:]
deletedAmId4 := ams[0].GetPublicId()
_, err = oidcRepo.DeleteAuthMethod(ctx, deletedAmId4)
require.NoError(t, err)
ams = ams[1:]
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
resp3, err := auth.ListAuthMethodsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, []string{org.PublicId}, false)
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 2)
assert.Empty(t, cmp.Diff(resp3.DeletedIds, []string{deletedAmId3, deletedAmId4}, cmpopts.SortSlices(func(a, b string) bool { return a < b })))
require.Empty(t, resp3.Items)
})
}
@@ -0,0 +1,73 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
"time"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListAuthMethodsPage lists up to page size auth methods, filtering out entries that
// do not pass the filter item function. It will automatically request
// more auth methods 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.
// Auth methods are ordered by create time descending (most recently created first).
func ListAuthMethodsPage(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[AuthMethod],
tok *listtoken.Token,
repo *AuthMethodRepository,
scopeIds []string,
withUnauthenticatedUser bool,
) (*pagination.ListResponse[AuthMethod], error) {
const op = "auth.ListAuthMethodsPage"
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")
case len(scopeIds) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope ids")
case tok.ResourceType != resource.AuthMethod:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have an auth method resource type")
}
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 AuthMethod, limit int) ([]AuthMethod, time.Time, error) {
opts := []Option{
WithLimit(ctx, limit),
}
if withUnauthenticatedUser {
opts = append(opts, WithUnauthenticatedUser(ctx, true))
}
if lastPageItem != nil {
return repo.List(ctx, scopeIds, lastPageItem, opts...)
}
lastItem, err := tok.LastItem(ctx)
if err != nil {
return nil, time.Time{}, err
}
return repo.List(ctx, scopeIds, lastItem, opts...)
}
return pagination.ListPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.EstimatedCount, tok)
}
@@ -0,0 +1,78 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
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"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListAuthMethodsRefresh lists up to page size auth methods, filtering out entries that
// do not pass the filter item function. It will automatically request
// more auth methods 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.
// Auth methods are ordered by update time descending (most recently updated first).
// Auth methods may contain items that were already returned during the initial
// pagination phase. It also returns a list of any auth methods deleted since the
// start of the initial pagination phase or last response.
func ListAuthMethodsRefresh(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[AuthMethod],
tok *listtoken.Token,
repo *AuthMethodRepository,
scopeIds []string,
withUnauthenticatedUser bool,
) (*pagination.ListResponse[AuthMethod], error) {
const op = "auth.ListAuthMethodsRefresh"
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")
case len(scopeIds) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope ids")
case tok.ResourceType != resource.AuthMethod:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have an auth method resource type")
}
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 AuthMethod, limit int) ([]AuthMethod, time.Time, error) {
opts := []Option{
WithLimit(ctx, limit),
}
if withUnauthenticatedUser {
opts = append(opts, WithUnauthenticatedUser(ctx, true))
}
// Add the database read timeout to account for any creations missed due to concurrent
// transactions in the initial pagination phase.
return repo.ListRefresh(ctx, scopeIds, rt.PreviousPhaseUpperBound.Add(-globals.RefreshReadLookbackDuration), lastPageItem, opts...)
}
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,86 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
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"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListAuthMethodsRefreshPage lists up to page size auth methods, filtering out entries that
// do not pass the filter item function. It will automatically request
// more auth methods 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.
// Auth methods are ordered by update time descending (most recently updated first).
// Auth methods may contain items that were already returned during the initial
// pagination phase. It also returns a list of any auth methods deleted since the
// last response.
func ListAuthMethodsRefreshPage(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[AuthMethod],
tok *listtoken.Token,
repo *AuthMethodRepository,
scopeIds []string,
withUnauthenticatedUser bool,
) (*pagination.ListResponse[AuthMethod], error) {
const op = "auth.ListAuthMethodsRefreshPage"
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")
case len(scopeIds) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope ids")
case tok.ResourceType != resource.AuthMethod:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have an auth method resource type")
}
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 AuthMethod, limit int) ([]AuthMethod, time.Time, error) {
// Add the database read timeout to account for any creations missed due to concurrent
// transactions in the initial pagination phase.
opts := []Option{
WithLimit(ctx, limit),
}
if withUnauthenticatedUser {
opts = append(opts, WithUnauthenticatedUser(ctx, true))
}
since := rt.PhaseLowerBound.Add(-globals.RefreshReadLookbackDuration)
if lastPageItem != nil {
return repo.ListRefresh(ctx, scopeIds, since, lastPageItem, opts...)
}
lastItem, err := tok.LastItem(ctx)
if err != nil {
return nil, time.Time{}, err
}
return repo.ListRefresh(ctx, scopeIds, since, lastItem, opts...)
}
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)
}