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:
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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
File renamed without changes.
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
`
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user