From 8d50a0913fd775d01f1a8cf9a904db2016b3bfba Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Thu, 21 Dec 2023 14:58:21 -0500 Subject: [PATCH] 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 --- internal/auth/additional_verification_test.go | 5 +- internal/auth/auth_method.go | 94 ++ internal/auth/db_test.go | 22 +- internal/auth/ldap/register.go | 62 ++ internal/auth/oidc/register.go | 60 ++ internal/auth/options.go | 64 ++ internal/auth/options_test.go | 84 ++ .../{authmethod.go => auth_method.go} | 0 ...authmethod_test.go => auth_method_test.go} | 0 internal/auth/password/register.go | 34 + internal/auth/query.go | 808 ++++++++++++++++++ internal/auth/registry.go | 68 ++ internal/auth/repository_auth_method.go | 197 +++++ internal/auth/repository_auth_method_test.go | 390 +++++++++ internal/auth/service_list_auth_methods.go | 54 ++ .../service_list_auth_methods_ext_test.go | 711 +++++++++++++++ .../auth/service_list_auth_methods_page.go | 73 ++ .../auth/service_list_auth_methods_refresh.go | 78 ++ .../service_list_auth_methods_refresh_page.go | 86 ++ internal/daemon/controller/common/common.go | 2 + internal/daemon/controller/controller.go | 5 + internal/daemon/controller/handler.go | 12 +- .../authmethods/authmethod_service.go | 299 ++++--- .../authmethods/authmethod_service_test.go | 507 ++++++++++- .../handlers/authmethods/ldap_test.go | 11 +- .../handlers/authmethods/oidc_test.go | 39 +- .../handlers/authmethods/password_test.go | 16 +- .../migrations/oss/postgres/2/10_auth.up.sql | 1 + .../10_auth_method_base_table_updates.up.sql | 154 ++++ .../sqltest/tests/pagination/auth_method.sql | 59 ++ internal/gen/controller.swagger.json | 46 +- .../api/services/auth_method_service.pb.go | 708 ++++++++------- .../api/services/v1/auth_method_service.proto | 30 + .../tests/api/authmethods/authmethod_test.go | 6 +- 34 files changed, 4340 insertions(+), 445 deletions(-) create mode 100644 internal/auth/auth_method.go create mode 100644 internal/auth/ldap/register.go create mode 100644 internal/auth/oidc/register.go create mode 100644 internal/auth/options.go create mode 100644 internal/auth/options_test.go rename internal/auth/password/{authmethod.go => auth_method.go} (100%) rename internal/auth/password/{authmethod_test.go => auth_method_test.go} (100%) create mode 100644 internal/auth/password/register.go create mode 100644 internal/auth/query.go create mode 100644 internal/auth/registry.go create mode 100644 internal/auth/repository_auth_method.go create mode 100644 internal/auth/repository_auth_method_test.go create mode 100644 internal/auth/service_list_auth_methods.go create mode 100644 internal/auth/service_list_auth_methods_ext_test.go create mode 100644 internal/auth/service_list_auth_methods_page.go create mode 100644 internal/auth/service_list_auth_methods_refresh.go create mode 100644 internal/auth/service_list_auth_methods_refresh_page.go create mode 100644 internal/db/schema/migrations/oss/postgres/80/10_auth_method_base_table_updates.up.sql create mode 100644 internal/db/sqltest/tests/pagination/auth_method.sql diff --git a/internal/auth/additional_verification_test.go b/internal/auth/additional_verification_test.go index c4a4ebe72..a0c1f8896 100644 --- a/internal/auth/additional_verification_test.go +++ b/internal/auth/additional_verification_test.go @@ -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) diff --git a/internal/auth/auth_method.go b/internal/auth/auth_method.go new file mode 100644 index 000000000..56f097f35 --- /dev/null +++ b/internal/auth/auth_method.go @@ -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) +} diff --git a/internal/auth/db_test.go b/internal/auth/db_test.go index 2bf87366b..02e83fd86 100644 --- a/internal/auth/db_test.go +++ b/internal/auth/db_test.go @@ -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) diff --git a/internal/auth/ldap/register.go b/internal/auth/ldap/register.go new file mode 100644 index 000000000..14a23726d --- /dev/null +++ b/internal/auth/ldap/register.go @@ -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 +} diff --git a/internal/auth/oidc/register.go b/internal/auth/oidc/register.go new file mode 100644 index 000000000..2b68f1d8c --- /dev/null +++ b/internal/auth/oidc/register.go @@ -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 +} diff --git a/internal/auth/options.go b/internal/auth/options.go new file mode 100644 index 000000000..34e5cfc87 --- /dev/null +++ b/internal/auth/options.go @@ -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 + } +} diff --git a/internal/auth/options_test.go b/internal/auth/options_test.go new file mode 100644 index 000000000..ef947dc28 --- /dev/null +++ b/internal/auth/options_test.go @@ -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) + }) +} diff --git a/internal/auth/password/authmethod.go b/internal/auth/password/auth_method.go similarity index 100% rename from internal/auth/password/authmethod.go rename to internal/auth/password/auth_method.go diff --git a/internal/auth/password/authmethod_test.go b/internal/auth/password/auth_method_test.go similarity index 100% rename from internal/auth/password/authmethod_test.go rename to internal/auth/password/auth_method_test.go diff --git a/internal/auth/password/register.go b/internal/auth/password/register.go new file mode 100644 index 000000000..bfb83ec34 --- /dev/null +++ b/internal/auth/password/register.go @@ -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 +} diff --git a/internal/auth/query.go b/internal/auth/query.go new file mode 100644 index 000000000..f4141f50a --- /dev/null +++ b/internal/auth/query.go @@ -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; +` +) diff --git a/internal/auth/registry.go b/internal/auth/registry.go new file mode 100644 index 000000000..dba928e0f --- /dev/null +++ b/internal/auth/registry.go @@ -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) +} diff --git a/internal/auth/repository_auth_method.go b/internal/auth/repository_auth_method.go new file mode 100644 index 000000000..6517042d1 --- /dev/null +++ b/internal/auth/repository_auth_method.go @@ -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 +} diff --git a/internal/auth/repository_auth_method_test.go b/internal/auth/repository_auth_method_test.go new file mode 100644 index 000000000..77456642e --- /dev/null +++ b/internal/auth/repository_auth_method_test.go @@ -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))) +} diff --git a/internal/auth/service_list_auth_methods.go b/internal/auth/service_list_auth_methods.go new file mode 100644 index 000000000..139735942 --- /dev/null +++ b/internal/auth/service_list_auth_methods.go @@ -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) +} diff --git a/internal/auth/service_list_auth_methods_ext_test.go b/internal/auth/service_list_auth_methods_ext_test.go new file mode 100644 index 000000000..f6c98d886 --- /dev/null +++ b/internal/auth/service_list_auth_methods_ext_test.go @@ -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) + }) +} diff --git a/internal/auth/service_list_auth_methods_page.go b/internal/auth/service_list_auth_methods_page.go new file mode 100644 index 000000000..4d9e8ae20 --- /dev/null +++ b/internal/auth/service_list_auth_methods_page.go @@ -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) +} diff --git a/internal/auth/service_list_auth_methods_refresh.go b/internal/auth/service_list_auth_methods_refresh.go new file mode 100644 index 000000000..4439e63c7 --- /dev/null +++ b/internal/auth/service_list_auth_methods_refresh.go @@ -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) +} diff --git a/internal/auth/service_list_auth_methods_refresh_page.go b/internal/auth/service_list_auth_methods_refresh_page.go new file mode 100644 index 000000000..f6cc4061a --- /dev/null +++ b/internal/auth/service_list_auth_methods_refresh_page.go @@ -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) +} diff --git a/internal/daemon/controller/common/common.go b/internal/daemon/controller/common/common.go index a147a25ef..286696d08 100644 --- a/internal/daemon/controller/common/common.go +++ b/internal/daemon/controller/common/common.go @@ -4,6 +4,7 @@ package common import ( + "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -28,6 +29,7 @@ type ( OidcAuthRepoFactory = oidc.OidcRepoFactory LdapAuthRepoFactory = ldap.RepoFactory PasswordAuthRepoFactory func() (*password.Repository, error) + AuthMethodRepoFactory func() (*auth.AuthMethodRepository, error) ServersRepoFactory func() (*server.Repository, error) StaticRepoFactory func() (*static.Repository, error) PluginHostRepoFactory func() (*pluginhost.Repository, error) diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index 5745068a1..868cee984 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -11,6 +11,7 @@ import ( "sync" "sync/atomic" + "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -134,6 +135,7 @@ type Controller struct { OidcRepoFn common.OidcAuthRepoFactory LdapRepoFn common.LdapAuthRepoFactory PasswordAuthRepoFn common.PasswordAuthRepoFactory + AuthMethodRepoFn common.AuthMethodRepoFactory ServersRepoFn common.ServersRepoFactory SessionRepoFn session.RepositoryFactory ConnectionRepoFn common.ConnectionRepoFactory @@ -422,6 +424,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { c.PasswordAuthRepoFn = func() (*password.Repository, error) { return password.NewRepository(ctx, dbase, dbase, c.kms) } + c.AuthMethodRepoFn = func() (*auth.AuthMethodRepository, error) { + return auth.NewAuthMethodRepository(ctx, dbase, dbase, c.kms) + } c.TargetRepoFn = func(o ...target.Option) (*target.Repository, error) { return target.NewRepository(ctx, dbase, dbase, c.kms, o...) } diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index aa4d4270f..c494553e1 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -164,7 +164,17 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error { services.RegisterAccountServiceServer(s, accts) } if _, ok := currentServices[services.AuthMethodService_ServiceDesc.ServiceName]; !ok { - authMethods, err := authmethods.NewService(c.baseContext, c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn, c.LdapRepoFn) + authMethods, err := authmethods.NewService( + c.baseContext, + c.kms, + c.PasswordAuthRepoFn, + c.OidcRepoFn, + c.IamRepoFn, + c.AuthTokenRepoFn, + c.LdapRepoFn, + c.AuthMethodRepoFn, + c.conf.RawConfig.Controller.MaxPageSize, + ) if err != nil { return fmt.Errorf("failed to create auth method handler service: %w", err) } diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_service.go b/internal/daemon/controller/handlers/authmethods/authmethod_service.go index f5ac58a8a..b6f5a72f6 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_service.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_service.go @@ -26,6 +26,8 @@ import ( "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/listtoken" + "github.com/hashicorp/boundary/internal/pagination" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/types/action" @@ -57,8 +59,6 @@ const ( authMethodIdField = "auth_method_id" tokenTypeField = "type" isPrimaryField = "is_primary" - - domain = "auth" ) var ( @@ -84,18 +84,31 @@ var ( type Service struct { pbs.UnsafeAuthMethodServiceServer - kms *kms.Kms - pwRepoFn common.PasswordAuthRepoFactory - oidcRepoFn common.OidcAuthRepoFactory - iamRepoFn common.IamRepoFactory - atRepoFn common.AuthTokenRepoFactory - ldapRepoFn common.LdapAuthRepoFactory + kms *kms.Kms + pwRepoFn common.PasswordAuthRepoFactory + oidcRepoFn common.OidcAuthRepoFactory + iamRepoFn common.IamRepoFactory + atRepoFn common.AuthTokenRepoFactory + ldapRepoFn common.LdapAuthRepoFactory + amRepoFn common.AuthMethodRepoFactory + maxPageSize uint } var _ pbs.AuthMethodServiceServer = (*Service)(nil) // NewService returns a auth method service which handles auth method related requests to boundary. -func NewService(ctx context.Context, kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoFn common.OidcAuthRepoFactory, iamRepoFn common.IamRepoFactory, atRepoFn common.AuthTokenRepoFactory, ldapRepoFn common.LdapAuthRepoFactory, opt ...handlers.Option) (Service, error) { +func NewService( + ctx context.Context, + kms *kms.Kms, + pwRepoFn common.PasswordAuthRepoFactory, + oidcRepoFn common.OidcAuthRepoFactory, + iamRepoFn common.IamRepoFactory, + atRepoFn common.AuthTokenRepoFactory, + ldapRepoFn common.LdapAuthRepoFactory, + amRepoFn common.AuthMethodRepoFactory, + maxPageSize uint, + opt ...handlers.Option, +) (Service, error) { const op = "authmethods.NewService" if kms == nil { return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing kms") @@ -115,15 +128,32 @@ func NewService(ctx context.Context, kms *kms.Kms, pwRepoFn common.PasswordAuthR if atRepoFn == nil { return Service{}, fmt.Errorf("nil auth token repository provided") } - s := Service{kms: kms, pwRepoFn: pwRepoFn, oidcRepoFn: oidcRepoFn, iamRepoFn: iamRepoFn, atRepoFn: atRepoFn, ldapRepoFn: ldapRepoFn} + if amRepoFn == nil { + return Service{}, fmt.Errorf("nil auth method repository provided") + } + if maxPageSize == 0 { + maxPageSize = uint(globals.DefaultMaxPageSize) + } + s := Service{ + kms: kms, + pwRepoFn: pwRepoFn, + oidcRepoFn: oidcRepoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + ldapRepoFn: ldapRepoFn, + amRepoFn: amRepoFn, + maxPageSize: maxPageSize, + } return s, nil } // ListAuthMethods implements the interface pbs.AuthMethodServiceServer. func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRequest) (*pbs.ListAuthMethodsResponse, error) { + const op = "authmethods.(Service).ListAuthMethods" + if err := validateListRequest(ctx, req); err != nil { - return nil, err + return nil, errors.Wrap(ctx, err, op) } authResults := s.authResult(ctx, req.GetScopeId(), action.List) if authResults.Error != nil { @@ -138,6 +168,21 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe return nil, authResults.Error } } + pageSize := int(s.maxPageSize) + // Use the requested page size only if it is smaller than + // the configured max. + if req.GetPageSize() != 0 && uint(req.GetPageSize()) < s.maxPageSize { + pageSize = int(req.GetPageSize()) + } + + withUnauthenticatedUser := false + reqCtx, ok := requests.RequestContextFromCtx(ctx) + if !ok { + return nil, errors.New(ctx, errors.Internal, op, "no request context found") + } + if reqCtx.UserId == globals.AnonymousUserId { + withUnauthenticatedUser = true + } scopeIds, scopeInfoMap, err := scopeids.GetListingScopeIds( ctx, s.iamRepoFn, authResults, req.GetScopeId(), resource.AuthMethod, req.GetRecursive()) @@ -149,63 +194,127 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe return &pbs.ListAuthMethodsResponse{}, nil } - ul, err := s.listFromRepo(ctx, scopeIds, authResults) - if err != nil { - return nil, err - } - if len(ul) == 0 { - return &pbs.ListAuthMethodsResponse{}, nil + var filterItemFn func(ctx context.Context, item auth.AuthMethod) (bool, error) + switch { + case req.GetFilter() != "": + // Only use a filter if we need to + filter, err := handlers.NewFilter(ctx, req.GetFilter()) + if err != nil { + return nil, err + } + // TODO: replace the need for this function with some way to convert the `filter` + // to a domain type. This would allow filtering to happen in the domain, and we could + // remove this callback altogether. + filterItemFn = func(ctx context.Context, item auth.AuthMethod) (bool, error) { + outputOpts, ok, err := newOutputOpts(ctx, item, scopeInfoMap, authResults) + if err != nil { + return false, errors.Wrap(ctx, err, op) + } + if !ok { + return false, nil + } + + pbItem, err := toAuthMethodProto(ctx, item, outputOpts...) + if err != nil { + return false, err + } + + filterable, err := subtypes.Filterable(ctx, pbItem) + if err != nil { + return false, err + } + return filter.Match(filterable), nil + } + default: + filterItemFn = func(ctx context.Context, item auth.AuthMethod) (bool, error) { + return true, nil + } } - filter, err := handlers.NewFilter(ctx, req.GetFilter()) + repo, err := s.amRepoFn() if err != nil { - return nil, err + return nil, errors.Wrap(ctx, err, op) } - finalItems := make([]*pb.AuthMethod, 0, len(ul)) - res := perms.Resource{ - Type: resource.AuthMethod, - } - for _, am := range ul { - res.Id = am.GetPublicId() - res.ScopeId = am.GetScopeId() - authorizedActions := authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(am.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings() - if len(authorizedActions) == 0 { - continue - } - outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId) - outputOpts := make([]handlers.Option, 0, 3) - outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields)) - if outputFields.Has(globals.ScopeField) { - outputOpts = append(outputOpts, handlers.WithScope(scopeInfoMap[am.GetScopeId()])) + grantsHash, err := authResults.GrantsHash(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var listResp *pagination.ListResponse[auth.AuthMethod] + var sortBy string + if req.GetListToken() == "" { + sortBy = "created_time" + listResp, err = auth.ListAuthMethods(ctx, grantsHash, pageSize, filterItemFn, repo, scopeIds, withUnauthenticatedUser) + if err != nil { + return nil, err } - if outputFields.Has(globals.AuthorizedActionsField) { - outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } else { + listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.AuthMethod, grantsHash) + if err != nil { + return nil, err } - if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + switch st := listToken.Subtype.(type) { + case *listtoken.PaginationToken: + sortBy = "created_time" + listResp, err = auth.ListAuthMethodsPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds, withUnauthenticatedUser) if err != nil { return nil, err } - outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) - } - - item, err := toAuthMethodProto(ctx, am, outputOpts...) - if err != nil { - return nil, err - } - - // This comes last so that we can use item fields in the filter after - // the allowed fields are populated above - filterable, err := subtypes.Filterable(ctx, item) - if err != nil { - return nil, err - } - if filter.Match(filterable) { - finalItems = append(finalItems, item) + case *listtoken.StartRefreshToken: + sortBy = "updated_time" + listResp, err = auth.ListAuthMethodsRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds, withUnauthenticatedUser) + if err != nil { + return nil, err + } + case *listtoken.RefreshToken: + sortBy = "updated_time" + listResp, err = auth.ListAuthMethodsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds, withUnauthenticatedUser) + if err != nil { + return nil, err + } + default: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st) } } - return &pbs.ListAuthMethodsResponse{Items: finalItems}, nil + + finalItems := make([]*pb.AuthMethod, 0, len(listResp.Items)) + for _, item := range listResp.Items { + outputOpts, ok, err := newOutputOpts(ctx, item, scopeInfoMap, authResults) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if !ok { + continue + } + + pbItem, err := toAuthMethodProto(ctx, item, outputOpts...) + if err != nil { + continue + } + finalItems = append(finalItems, pbItem) + } + respType := "delta" + if listResp.CompleteListing { + respType = "complete" + } + resp := &pbs.ListAuthMethodsResponse{ + Items: finalItems, + EstItemCount: uint32(listResp.EstimatedItemCount), + RemovedIds: listResp.DeletedIds, + ResponseType: respType, + SortBy: sortBy, + SortDir: "desc", + } + + if listResp.ListToken != nil { + resp.ListToken, err = handlers.MarshalListToken(ctx, listResp.ListToken, pbs.ResourceType_RESOURCE_TYPE_AUTH_METHOD) + if err != nil { + return nil, err + } + } + + return resp, nil } // GetAuthMethod implements the interface pbs.AuthMethodServiceServer. @@ -499,54 +608,6 @@ func (s Service) getFromRepo(ctx context.Context, id string) (auth.AuthMethod, e return am, nil } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string, authResults requestauth.VerifyResults) ([]auth.AuthMethod, error) { - const op = "authmethods.(Service).listFromRepo" - reqCtx, ok := requests.RequestContextFromCtx(ctx) - if !ok { - return nil, errors.New(ctx, errors.Internal, op, "no request context found") - } - - var outUl []auth.AuthMethod - - oidcRepo, err := s.oidcRepoFn() - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - ol, err := oidcRepo.ListAuthMethods(ctx, scopeIds, oidc.WithUnauthenticatedUser(reqCtx.UserId == globals.AnonymousUserId), oidc.WithLimit(-1)) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - for _, item := range ol { - outUl = append(outUl, item) - } - - repo, err := s.pwRepoFn() - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - pl, err := repo.ListAuthMethods(ctx, scopeIds) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - for _, item := range pl { - outUl = append(outUl, item) - } - - ldapRepo, err := s.ldapRepoFn() - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - ll, err := ldapRepo.ListAuthMethods(ctx, scopeIds, ldap.WithUnauthenticatedUser(ctx, reqCtx.UserId == globals.AnonymousUserId)) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - for _, item := range ll { - outUl = append(outUl, item) - } - - return outUl, nil -} - func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (auth.AuthMethod, error) { const op = "authmethods.(Service).createInRepo" var out auth.AuthMethod @@ -1508,3 +1569,33 @@ func transformAuthenticateResponseAttributes(ctx context.Context, msg proto.Mess } return nil } + +func newOutputOpts(ctx context.Context, item auth.AuthMethod, scopeInfoMap map[string]*scopes.ScopeInfo, authResults requestauth.VerifyResults) ([]handlers.Option, bool, error) { + res := perms.Resource{ + Type: resource.AuthMethod, + } + res.Id = item.GetPublicId() + res.ScopeId = item.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(item.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { + return nil, false, nil + } + + outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId) + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields)) + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(scopeInfoMap[item.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, item.GetPublicId()) + if err != nil { + return nil, false, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + return outputOpts, true, nil +} diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go b/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go index c208c1ca3..950794d2c 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go @@ -19,6 +19,7 @@ import ( "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" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -29,8 +30,11 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/requests" + "github.com/hashicorp/boundary/internal/server" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods" @@ -112,6 +116,9 @@ func TestGet(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kmsCache) } + authMethodRepoFn := func() (*auth.AuthMethodRepository, error) { + return auth.NewAuthMethodRepository(ctx, rw, rw, kmsCache) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) @@ -257,7 +264,7 @@ func TestGet(t *testing.T) { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(err, "Couldn't create new auth_method service.") got, gErr := s.GetAuthMethod(requestauth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req) @@ -273,14 +280,14 @@ func TestGet(t *testing.T) { assert.Empty(cmp.Diff( got, tc.res, - protocmp.Transform(), - protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"), cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), + protocmp.Transform(), + protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"), ), "GetAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } @@ -307,6 +314,9 @@ func TestList(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kmsCache) } + authMethodRepoFn := func() (*auth.AuthMethodRepository, error) { + return auth.NewAuthMethodRepository(ctx, rw, rw, kmsCache) + } iamRepo := iam.TestRepo(t, conn, wrapper) oNoAuthMethods, _ := iam.TestScopes(t, iamRepo) @@ -435,17 +445,33 @@ func TestList(t *testing.T) { { name: "List Some Auth Methods", req: &pbs.ListAuthMethodsRequest{ScopeId: oWithAuthMethods.GetPublicId()}, - res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantSomeAuthMethods)}, + res: &pbs.ListAuthMethodsResponse{ + Items: cpSorted(wantSomeAuthMethods), + EstItemCount: 5, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "List Other Auth Methods", req: &pbs.ListAuthMethodsRequest{ScopeId: oWithOtherAuthMethods.GetPublicId()}, - res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantOtherAuthMethods)}, + res: &pbs.ListAuthMethodsResponse{ + Items: cpSorted(wantOtherAuthMethods), + EstItemCount: 3, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "List No Auth Methods", req: &pbs.ListAuthMethodsRequest{ScopeId: oNoAuthMethods.GetPublicId()}, - res: &pbs.ListAuthMethodsResponse{}, + res: &pbs.ListAuthMethodsResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "Unfound Auth Method", @@ -459,6 +485,10 @@ func TestList(t *testing.T) { Items: func() []*pb.AuthMethod { return cpSorted(append(wantSomeAuthMethods, wantOtherAuthMethods...)) }(), + EstItemCount: 8, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", }, }, { @@ -467,12 +497,22 @@ func TestList(t *testing.T) { ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithAuthMethods.GetPublicId()), }, - res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantSomeAuthMethods)}, + res: &pbs.ListAuthMethodsResponse{ + Items: cpSorted(wantSomeAuthMethods), + EstItemCount: 5, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "Filter All Auth Methods", req: &pbs.ListAuthMethodsRequest{ScopeId: oWithAuthMethods.GetPublicId(), Filter: `"/item/id"=="nothingmatchesthis"`}, - res: &pbs.ListAuthMethodsResponse{}, + res: &pbs.ListAuthMethodsResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, }, { name: "Filter Bad Format", @@ -483,7 +523,7 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(err, "Couldn't create new auth_method service.") // First check with non-anonymous user @@ -501,18 +541,17 @@ func TestList(t *testing.T) { } slices.SortFunc(got.Items, sorterFn) - assert.Empty(cmp.Diff( - got, - tc.res, - protocmp.Transform(), - protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"), - protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac"), + assert.Empty(cmp.Diff(got, tc.res, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListAuthMethodsResponse{}, "list_token"), + protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"), + protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac"), ), "ListAuthMethods() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Now check with anonymous user @@ -550,6 +589,9 @@ func TestDelete(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kmsCache) } + authMethodRepoFn := func() (*auth.AuthMethodRepository, error) { + return auth.NewAuthMethodRepository(ctx, rw, rw, kmsCache) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) @@ -563,7 +605,7 @@ func TestDelete(t *testing.T) { ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), []string{"ldaps://ldap1"}) - s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(t, err, "Error when getting new auth_method service.") cases := []struct { @@ -643,12 +685,15 @@ func TestDelete_twice(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } + authMethodRepoFn := func() (*auth.AuthMethodRepository, error) { + return auth.NewAuthMethodRepository(ctx, rw, rw, kms) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] - s, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(err, "Error when getting new auth_method service.") req := &pbs.DeleteAuthMethodRequest{ @@ -682,6 +727,9 @@ func TestCreate(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, testKms) } + authMethodRepoFn := func() (*auth.AuthMethodRepository, error) { + return auth.NewAuthMethodRepository(ctx, rw, rw, testKms) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) @@ -1478,7 +1526,7 @@ func TestCreate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(ctx, testKms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, testKms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(err, "Error when getting new auth_method service.") got, gErr := s.CreateAuthMethod(requestauth.DisabledAuthTestContext(iamRepoFn, tc.req.GetItem().GetScopeId()), tc.req) @@ -1550,3 +1598,426 @@ func TestCreate(t *testing.T) { }) } } + +func TestListPagination(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) + sqlDB, err := conn.SqlDB(ctx) + require.NoError(t, err) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kmsCache) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(ctx, rw, rw, kmsCache) + } + tokenRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(ctx, rw, rw, kmsCache) + } + serversRepoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, kmsCache) + } + authMethodRepoFn := func() (*auth.AuthMethodRepository, error) { + return auth.NewAuthMethodRepository(ctx, rw, rw, kmsCache) + } + + iamRepo, err := iamRepoFn() + require.NoError(t, err) + tokenRepo, err := tokenRepoFn() + require.NoError(t, err) + ldapRepo, err := ldapRepoFn() + require.NoError(t, err) + oidcRepo, err := oidcRepoFn() + require.NoError(t, err) + + orgNoAms, _ := iam.TestScopes(t, iamRepo) + org, proj := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(t, err) + + var allAuthMethods []*pb.AuthMethod + + oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePublicState, "alice_rp", "secret", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]), oidc.WithSigningAlgs(oidc.EdDSA), + oidc.WithPrompts(oidc.Consent)) + iam.TestSetPrimaryAuthMethod(t, iamRepo, org, oidcam.GetPublicId()) + + allAuthMethods = append(allAuthMethods, &pb.AuthMethod{ + Id: oidcam.GetPublicId(), + ScopeId: org.GetPublicId(), + CreatedTime: oidcam.GetCreateTime().GetTimestamp(), + UpdatedTime: oidcam.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 2, + Type: oidc.Subtype.String(), + Attrs: &pb.AuthMethod_OidcAuthMethodsAttributes{ + OidcAuthMethodsAttributes: &pb.OidcAuthMethodAttributes{ + Issuer: wrapperspb.String("https://alice.com"), + ClientId: wrapperspb.String("alice_rp"), + ClientSecretHmac: "", + State: string(oidc.ActivePublicState), + ApiUrlPrefix: wrapperspb.String("https://api.com"), + CallbackUrl: fmt.Sprintf(oidc.CallbackEndpoint, "https://api.com"), + SigningAlgorithms: []string{ + string(oidc.EdDSA), + }, + Prompts: []string{string(oidc.Consent)}, + }, + }, + IsPrimary: true, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }) + + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"}, + ldap.WithOperationalState(ctx, ldap.ActivePublicState), + ldap.WithMaximumPageSize(ctx, 10), + ldap.WithDerefAliases(ctx, ldap.DerefAlways), + ) + allAuthMethods = append(allAuthMethods, &pb.AuthMethod{ + Id: ldapAm.GetPublicId(), + ScopeId: org.GetPublicId(), + CreatedTime: ldapAm.GetCreateTime().GetTimestamp(), + UpdatedTime: ldapAm.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + State: string(ldap.ActivePublicState), + Urls: []string{"ldaps://ldap1"}, + MaximumPageSize: 10, + DereferenceAliases: wrapperspb.String(string(ldap.DerefAlways)), + }, + }, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }) + + for _, am := range password.TestAuthMethods(t, conn, org.GetPublicId(), 7) { + allAuthMethods = append(allAuthMethods, &pb.AuthMethod{ + Id: am.GetPublicId(), + ScopeId: org.GetPublicId(), + CreatedTime: am.GetCreateTime().GetTimestamp(), + UpdatedTime: am.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: "password", + Attrs: &pb.AuthMethod_PasswordAuthMethodAttributes{ + PasswordAuthMethodAttributes: &pb.PasswordAuthMethodAttributes{ + MinPasswordLength: 8, + MinLoginNameLength: 3, + }, + }, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }) + } + + masterAuthMethod := password.TestAuthMethods(t, conn, org.GetPublicId(), 1)[0] + acct := password.TestAccount(t, conn, masterAuthMethod.GetPublicId(), "test_user") + u := iam.TestUser(t, iamRepo, org.GetPublicId(), iam.WithAccountIds(acct.PublicId)) + orgRole := iam.TestRole(t, conn, org.GetPublicId()) + iam.TestRoleGrant(t, conn, orgRole.GetPublicId(), "id=*;type=*;actions=*") + iam.TestUserRole(t, conn, orgRole.GetPublicId(), u.GetPublicId()) + projRole := iam.TestRole(t, conn, proj.GetPublicId()) + iam.TestRoleGrant(t, conn, projRole.GetPublicId(), "id=*;type=*;actions=*") + iam.TestUserRole(t, conn, projRole.GetPublicId(), u.GetPublicId()) + at, err := tokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId()) + require.NoError(t, err) + + allAuthMethods = append(allAuthMethods, &pb.AuthMethod{ + Id: masterAuthMethod.GetPublicId(), + ScopeId: org.GetPublicId(), + CreatedTime: masterAuthMethod.GetCreateTime().GetTimestamp(), + UpdatedTime: masterAuthMethod.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: "password", + Attrs: &pb.AuthMethod_PasswordAuthMethodAttributes{ + PasswordAuthMethodAttributes: &pb.PasswordAuthMethodAttributes{ + MinPasswordLength: 8, + MinLoginNameLength: 3, + }, + }, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }) + + // Reverse slice since we're sorting by create time descending + slices.Reverse(allAuthMethods) + + // Run analyze to update postgres meta tables + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // Test without anon user + requestInfo := authpb.RequestInfo{ + TokenFormat: uint32(requestauth.AuthTokenTypeBearer), + PublicId: at.GetPublicId(), + Token: at.GetToken(), + } + requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + ctx = requestauth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kmsCache, &requestInfo) + + s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, tokenRepoFn, ldapRepoFn, authMethodRepoFn, 1000) + require.NoError(t, err) + + cmpOptions := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"), + protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac"), + protocmp.IgnoreFields(&pbs.ListAuthMethodsResponse{}, "list_token"), + } + + // Start paginating, recursively + req := &pbs.ListAuthMethodsRequest{ + ScopeId: org.PublicId, + Filter: "", + ListToken: "", + PageSize: 2, + } + got, err := s.ListAuthMethods(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + assert.Empty( + t, + cmp.Diff( + got, + &pbs.ListAuthMethodsResponse{ + Items: allAuthMethods[0:2], + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpOptions..., + ), + ) + + // Request second page + req.ListToken = got.ListToken + got, err = s.ListAuthMethods(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + assert.Empty( + t, + cmp.Diff( + got, + &pbs.ListAuthMethodsResponse{ + Items: allAuthMethods[2:4], + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpOptions..., + ), + ) + + // Request rest of results + req.ListToken = got.ListToken + req.PageSize = 10 + got, err = s.ListAuthMethods(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 6) + assert.Empty( + t, + cmp.Diff( + got, + &pbs.ListAuthMethodsResponse{ + Items: allAuthMethods[4:], + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpOptions..., + ), + ) + + // create another auth method + am := password.TestAuthMethods(t, conn, org.GetPublicId(), 1)[0] + ampb := &pb.AuthMethod{ + Id: am.GetPublicId(), + ScopeId: org.GetPublicId(), + CreatedTime: am.GetCreateTime().GetTimestamp(), + UpdatedTime: am.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: "password", + Attrs: &pb.AuthMethod_PasswordAuthMethodAttributes{ + PasswordAuthMethodAttributes: &pb.PasswordAuthMethodAttributes{ + MinPasswordLength: 8, + MinLoginNameLength: 3, + }, + }, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + } + // add to front since it's most recently updated + allAuthMethods = append([]*pb.AuthMethod{ampb}, allAuthMethods...) + + // delete a different auth method + _, err = oidcRepo.DeleteAuthMethod(ctx, allAuthMethods[len(allAuthMethods)-1].Id) + require.NoError(t, err) + deletedAM := allAuthMethods[len(allAuthMethods)-1] + allAuthMethods = allAuthMethods[:len(allAuthMethods)-1] + + // update a different auth method + ldapAm.AuthMethod.Description = "new-description" + allAuthMethods[len(allAuthMethods)-1].Description = wrapperspb.String("new-description") + updatedAM, _, err := ldapRepo.UpdateAuthMethod(ctx, ldapAm, ldapAm.Version, []string{"description"}) + require.NoError(t, err) + allAuthMethods[len(allAuthMethods)-1].UpdatedTime = updatedAM.UpdateTime.GetTimestamp() + allAuthMethods[len(allAuthMethods)-1].Version = updatedAM.GetVersion() + // add to front since it's most recently updated + allAuthMethods = append([]*pb.AuthMethod{allAuthMethods[len(allAuthMethods)-1]}, allAuthMethods[:len(allAuthMethods)-1]...) + + // request updated results + req.ListToken = got.ListToken + req.PageSize = 1 + got, err = s.ListAuthMethods(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty( + t, + cmp.Diff( + got, + &pbs.ListAuthMethodsResponse{ + Items: []*pb.AuthMethod{allAuthMethods[0]}, + ResponseType: "delta", + ListToken: "", + SortBy: "updated_time", + SortDir: "desc", + // should be the deleted auth method + RemovedIds: []string{deletedAM.Id}, + EstItemCount: 10, + }, + cmpOptions..., + ), + ) + + // get next page + req.ListToken = got.ListToken + got, err = s.ListAuthMethods(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty( + t, + cmp.Diff( + got, + &pbs.ListAuthMethodsResponse{ + Items: []*pb.AuthMethod{allAuthMethods[1]}, + ResponseType: "complete", + ListToken: "", + SortBy: "updated_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpOptions..., + ), + ) + + // Request new page with filter requiring looping + // to fill the page. + req.ListToken = "" + req.PageSize = 1 + req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, allAuthMethods[len(allAuthMethods)-2].Id, allAuthMethods[len(allAuthMethods)-1].Id) + got, err = s.ListAuthMethods(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty( + t, + cmp.Diff( + got, + &pbs.ListAuthMethodsResponse{ + Items: []*pb.AuthMethod{allAuthMethods[len(allAuthMethods)-2]}, + ResponseType: "delta", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + // Should be empty again + RemovedIds: nil, + EstItemCount: 10, + }, + cmpOptions..., + ), + ) + req.ListToken = got.ListToken + // Get the second page + got, err = s.ListAuthMethods(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty( + t, + cmp.Diff( + got, + &pbs.ListAuthMethodsResponse{ + Items: []*pb.AuthMethod{allAuthMethods[len(allAuthMethods)-1]}, + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + cmpOptions..., + ), + ) + req.ListToken = got.ListToken + + // List items in the empty scope + req = &pbs.ListAuthMethodsRequest{ + ScopeId: orgNoAms.PublicId, + Filter: "", + ListToken: "", + PageSize: 2, + } + got, err = s.ListAuthMethods(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 0) + // Compare without comparing the list token + assert.Empty( + t, + cmp.Diff( + got, + &pbs.ListAuthMethodsResponse{ + Items: nil, + ResponseType: "complete", + ListToken: "", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + }, + cmpOptions..., + ), + ) +} diff --git a/internal/daemon/controller/handlers/authmethods/ldap_test.go b/internal/daemon/controller/handlers/authmethods/ldap_test.go index 032c4a8cc..f35ea941d 100644 --- a/internal/daemon/controller/handlers/authmethods/ldap_test.go +++ b/internal/daemon/controller/handlers/authmethods/ldap_test.go @@ -16,6 +16,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" + am "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -67,10 +68,13 @@ func Test_UpdateLdap(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, rw, rw, kms) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) - tested, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + tested, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(t, err, "Error when getting new auth_method service.") defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} @@ -915,6 +919,9 @@ func TestAuthenticate_Ldap(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(testCtx, testRw, testRw, testKms) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(testCtx, testRw, testRw, testKms) + } orgDbWrapper, err := testKms.GetWrapper(testCtx, o.GetPublicId(), kms.KeyPurposeDatabase) require.NoError(t, err) @@ -1094,7 +1101,7 @@ func TestAuthenticate_Ldap(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(testCtx, testKms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(testCtx, testKms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request) diff --git a/internal/daemon/controller/handlers/authmethods/oidc_test.go b/internal/daemon/controller/handlers/authmethods/oidc_test.go index 938fc3d66..c445695aa 100644 --- a/internal/daemon/controller/handlers/authmethods/oidc_test.go +++ b/internal/daemon/controller/handlers/authmethods/oidc_test.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/boundary/globals" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + am "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -63,6 +64,8 @@ type setup struct { ldapRepoFn common.LdapAuthRepoFactory pwRepoFn common.PasswordAuthRepoFactory atRepoFn common.AuthTokenRepoFactory + authMethodRepoFn common.AuthMethodRepoFactory + maxPageSize int org *iam.Scope proj *iam.Scope databaseWrapper wrapping.Wrapper @@ -104,12 +107,26 @@ func getSetup(t *testing.T) setup { ret.atRepoFn = func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, ret.rw, ret.rw, ret.kmsCache) } + ret.authMethodRepoFn = func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, ret.rw, ret.rw, ret.kmsCache) + } + ret.maxPageSize = 1000 ret.org, ret.proj = iam.TestScopes(t, ret.iamRepo) ret.databaseWrapper, err = ret.kmsCache.GetWrapper(ret.ctx, ret.org.PublicId, kms.KeyPurposeDatabase) require.NoError(err) - ret.authMethodService, err = authmethods.NewService(ret.ctx, ret.kmsCache, ret.pwRepoFn, ret.oidcRepoFn, ret.iamRepoFn, ret.atRepoFn, ret.ldapRepoFn) + ret.authMethodService, err = authmethods.NewService( + ret.ctx, + ret.kmsCache, + ret.pwRepoFn, + ret.oidcRepoFn, + ret.iamRepoFn, + ret.atRepoFn, + ret.ldapRepoFn, + ret.authMethodRepoFn, + uint(ret.maxPageSize), + ) require.NoError(err) ret.testProvider = capoidc.StartTestProvider(t) @@ -168,6 +185,9 @@ func TestList_FilterNonPublic(t *testing.T) { serversRepoFn := func() (*server.Repository, error) { return server.NewRepository(ctx, rw, rw, kmsCache) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, rw, rw, kmsCache) + } iamRepo := iam.TestRepo(t, conn, wrapper) @@ -195,7 +215,7 @@ func TestList_FilterNonPublic(t *testing.T) { oidc.WithIssuer(oidc.TestConvertToUrls(t, fmt.Sprintf("https://alice%d.com", i))[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])) } - s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(t, err, "Couldn't create new auth_method service.") req := &pbs.ListAuthMethodsRequest{ @@ -270,10 +290,13 @@ func TestUpdate_OIDC(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, rw, rw, kms) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) - tested, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + tested, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(t, err, "Error when getting new auth_method service.") defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} @@ -1139,6 +1162,9 @@ func TestUpdate_OIDCDryRun(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kmsCache) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, rw, rw, kmsCache) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) @@ -1192,7 +1218,7 @@ func TestUpdate_OIDCDryRun(t *testing.T) { }, } - tested, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + tested, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(t, err, "Error when getting new auth_method service.") cases := []struct { name string @@ -1339,6 +1365,9 @@ func TestChangeState_OIDC(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kmsCache) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, rw, rw, kmsCache) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) @@ -1362,7 +1391,7 @@ func TestChangeState_OIDC(t *testing.T) { mismatchedAM := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, "inactive", "different_client_id", oidc.ClientSecret(tpClientSecret), oidc.WithIssuer(oidc.TestConvertToUrls(t, tp.Addr())[0]), oidc.WithSigningAlgs(oidc.EdDSA), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://example.callback:58")[0]), oidc.WithCertificates(tpCert...)) - s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(t, err, "Error when getting new auth_method service.") wantTemplate := &pb.AuthMethod{ diff --git a/internal/daemon/controller/handlers/authmethods/password_test.go b/internal/daemon/controller/handlers/authmethods/password_test.go index c4051f79a..e78aaf25a 100644 --- a/internal/daemon/controller/handlers/authmethods/password_test.go +++ b/internal/daemon/controller/handlers/authmethods/password_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" + am "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -62,10 +63,13 @@ func TestUpdate_Password(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, rw, rw, kms) + } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) - tested, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + tested, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(t, err, "Error when getting new auth_method service.") defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} @@ -510,6 +514,9 @@ func TestAuthenticate_Password(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, rw, rw, kms) + } am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, conn, wrapper), o, am.PublicId) @@ -652,7 +659,7 @@ func TestAuthenticate_Password(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request) @@ -718,6 +725,9 @@ func TestAuthenticate_AuthAccountConnectedToIamUser_Password(t *testing.T) { atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } + authMethodRepoFn := func() (*am.AuthMethodRepository, error) { + return am.NewAuthMethodRepository(ctx, rw, rw, kms) + } am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] acct, err := password.NewAccount(ctx, am.GetPublicId(), password.WithLoginName(testLoginName)) @@ -735,7 +745,7 @@ func TestAuthenticate_AuthAccountConnectedToIamUser_Password(t *testing.T) { iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acct.GetPublicId()) require.NoError(err) - s, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + s, err := authmethods.NewService(ctx, kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn, authMethodRepoFn, 1000) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), diff --git a/internal/db/schema/migrations/oss/postgres/2/10_auth.up.sql b/internal/db/schema/migrations/oss/postgres/2/10_auth.up.sql index 48f4f8a5b..8177dc215 100644 --- a/internal/db/schema/migrations/oss/postgres/2/10_auth.up.sql +++ b/internal/db/schema/migrations/oss/postgres/2/10_auth.up.sql @@ -27,6 +27,7 @@ where -- migration 07_auth.up.sql This new definition also inserts the sub type's name -- into the base type. The name column must be on the base type, so the database -- can ensure that auth method names are unique across all sub types. +-- Replaced in 80/09_auth_method_base_table_updates.up.sql create or replace function insert_auth_method_subtype() returns trigger as $$ begin diff --git a/internal/db/schema/migrations/oss/postgres/80/10_auth_method_base_table_updates.up.sql b/internal/db/schema/migrations/oss/postgres/80/10_auth_method_base_table_updates.up.sql new file mode 100644 index 000000000..9f6b792bf --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/80/10_auth_method_base_table_updates.up.sql @@ -0,0 +1,154 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + -- Add create_time, update_time and is_active_public_state to auth_method table. + alter table auth_method + add column create_time wt_timestamp, + add column update_time wt_timestamp, + add column is_active_public_state boolean not null default false + ; + + -- Update rows with current values from LDAP auth method + with + sub_auth_method (public_id, create_time, update_time, is_active_public_state) as ( + select public_id, + create_time, + update_time, + state = 'active-public' + from auth_ldap_method + ) + update auth_method + set create_time = sub_auth_method.create_time, + update_time = sub_auth_method.update_time, + is_active_public_state = sub_auth_method.is_active_public_state + from sub_auth_method + where auth_method.public_id = sub_auth_method.public_id; + + -- Update rows with current values from OIDC auth method + with + sub_auth_method (public_id, create_time, update_time, is_active_public_state) as ( + select public_id, + create_time, + update_time, + state = 'active-public' + from auth_oidc_method + ) + update auth_method + set create_time = sub_auth_method.create_time, + update_time = sub_auth_method.update_time, + is_active_public_state = sub_auth_method.is_active_public_state + from sub_auth_method + where auth_method.public_id = sub_auth_method.public_id; + + -- Update rows with current values from password auth method + with + sub_auth_method (public_id, create_time, update_time, is_active_public_state) as ( + select public_id, + create_time, + update_time, + true + from auth_password_method + ) + update auth_method + set create_time = sub_auth_method.create_time, + update_time = sub_auth_method.update_time, + is_active_public_state = sub_auth_method.is_active_public_state + from sub_auth_method + where auth_method.public_id = sub_auth_method.public_id; + + -- Replace the insert trigger to also set the create_time + -- Partially replaces the insert_auth_method_subtype function defined in 2/10_auth.up.sql + -- This is because ldap and oidc subtypes have a state column, but password does not. + create or replace function insert_auth_method_subtype() returns trigger + as $$ + begin + insert into auth_method + (public_id, scope_id, name, create_time, is_active_public_state) + values + (new.public_id, new.scope_id, new.name, new.create_time, new.state = 'active-public'); + return new; + end; + $$ language plpgsql; + comment on function insert_auth_method_subtype() is + 'insert_auth_method_subtype() inserts sub type name, create time, update time, ' + 'and whether it is an active public state into the base type auth method table, ' + 'specifically for ldap or oidc subtypes.'; + + -- Replace the insert trigger to also set the create_time + -- Partially replaces the insert_auth_method_subtype function defined in 2/10_auth.up.sql + -- This is because ldap and oidc subtypes have a state column, but password does not. + create function insert_auth_method_password_subtype() returns trigger + as $$ + begin + insert into auth_method + (public_id, scope_id, name, create_time, is_active_public_state) + values + (new.public_id, new.scope_id, new.name, new.create_time, true); + return new; + end; + $$ language plpgsql; + comment on function insert_auth_method_password_subtype() is + 'insert_auth_method_password_subtype() inserts sub type name, create time, update time, ' + 'and that it is an active public state into the base type auth method table, ' + 'specifically for the password subtype.'; + + -- Add trigger to update the new update_time column on every subtype update. + create function update_auth_method_table_update_time() returns trigger + as $$ + begin + update auth_method set update_time = now() where public_id = new.public_id; + return new; + end; + $$ language plpgsql; + comment on function update_auth_method_table_update_time() is + 'update_auth_method_table_update_time is used to automatically update the update_time ' + 'of the base table whenever one of the subtype tables are updated'; + + -- Add trigger to update the new is_active_public_state column on ldap and oidc subtype update. + -- Password subtype has no state and will always be considered active public. + create function update_auth_method_table_is_active_public_state() returns trigger + as $$ + begin + if new.state = 'active-public' then + update auth_method set is_active_public_state = true where public_id = new.public_id; + return new; + else + update auth_method set is_active_public_state = false where public_id = new.public_id; + return new; + end if; + end; + $$ language plpgsql; + comment on function update_auth_method_table_is_active_public_state() is + 'update_auth_method_table_is_active_public_state is used to automatically update the ' + 'is_active_public_state column of the base table whenever one of the subtype tables are updated'; + + -- Add triggers to subtype tables + create trigger update_auth_method_table_update_time before update on auth_ldap_method + for each row execute procedure update_auth_method_table_update_time(); + create trigger update_auth_method_table_update_time before update on auth_oidc_method + for each row execute procedure update_auth_method_table_update_time(); + create trigger update_auth_method_table_update_time before update on auth_password_method + for each row execute procedure update_auth_method_table_update_time(); + + -- Replace trigger on password subtype table + drop trigger insert_auth_method_subtype on auth_password_method; + create trigger insert_auth_method_password_subtype before insert on auth_password_method + for each row execute procedure insert_auth_method_password_subtype(); + + -- Add is_active_public_state triggers to ldap and oidc tables, as password has no state column. + create trigger update_auth_method_table_is_active_public_state before update on auth_ldap_method + for each row execute procedure update_auth_method_table_is_active_public_state(); + create trigger update_auth_method_table_is_active_public_state before update on auth_oidc_method + for each row execute procedure update_auth_method_table_is_active_public_state(); + + -- Add new indexes for the create and update time queries. + create index auth_method_create_time_public_id_idx + on auth_method (create_time desc, public_id desc); + create index auth_method_update_time_public_id_idx + on auth_method (update_time desc, public_id desc); + + analyze auth_method; + +commit; diff --git a/internal/db/sqltest/tests/pagination/auth_method.sql b/internal/db/sqltest/tests/pagination/auth_method.sql new file mode 100644 index 000000000..0d1dd7158 --- /dev/null +++ b/internal/db/sqltest/tests/pagination/auth_method.sql @@ -0,0 +1,59 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + select plan(17); + -- load to test with ldap auth method + select wtt_load('widgets', 'iam', 'kms', 'auth'); + + -- validate the setup data + select is(count(*), 1::bigint) from auth_ldap_account where public_id = 'ala___walter'; + select is(count(*), 1::bigint) from auth_account where public_id = 'ala___walter'; + + -- validate the insert triggers + prepare insert_ldap_account as + insert into auth_ldap_account + (auth_method_id, public_id, login_name) + values + ('alm___widget', 'ala___tania', 'tania'); + select lives_ok('insert_ldap_account'); + + -- Verify the trigger functions exist and are declared properly + select has_function('update_auth_method_table_update_time'); + select volatility_is('update_auth_method_table_update_time', 'volatile'); + select isnt_strict('update_auth_method_table_update_time'); + select has_trigger('auth_ldap_method', 'update_auth_method_table_update_time'); + select has_trigger('auth_oidc_method', 'update_auth_method_table_update_time'); + select has_trigger('auth_password_method', 'update_auth_method_table_update_time'); + select has_trigger('auth_password_method', 'insert_auth_method_password_subtype'); + select has_trigger('auth_ldap_method', 'update_auth_method_table_is_active_public_state'); + select has_trigger('auth_oidc_method', 'update_auth_method_table_is_active_public_state'); + select has_index('auth_method', 'auth_method_create_time_public_id_idx', array['create_time', 'public_id']); + select has_index('auth_method', 'auth_method_update_time_public_id_idx', array['update_time', 'public_id']); + + -- To test that the trigger that sets the create_time of the base table + -- when an insert into a subtype table happens works, we check that the + -- create time of the entries in both tables match + prepare auth_method_create_time as select create_time from auth_method where public_id='apm___colors'; + prepare auth_password_method_create_time as select create_time from auth_password_method where public_id='apm___colors'; + select results_eq('auth_method_create_time','auth_password_method_create_time'); + + -- To test the trigger that updates the update_time of the base table, + -- we update one of the existing subtypes and check that the base table + -- entry has the same update_time as the subtype one. + update auth_password_method set name='blue black am' where public_id='apm___colors'; + prepare auth_method_update_time as select update_time from auth_method where public_id='apm___colors'; + prepare auth_password_method_update_time as select update_time from auth_password_method where public_id='apm___colors'; + select results_eq('auth_method_update_time','auth_password_method_update_time'); + + -- To test the trigger that updates the is_active_public_state of the base table, + -- we set one of the existing subtypes states to 'active-public' and check that the base table + -- entry has updated its column to true. + update auth_ldap_method set state='active-public' where public_id='ala___tania'; + prepare auth_method_is_active_public_state as select is_active_public_state from auth_method where public_id='ala___tania'; + prepare auth_ldap_method_state_is_active_public_state as select state = 'active-public' from auth_ldap_method where public_id='ala___tania'; + select results_eq('auth_method_is_active_public_state','auth_ldap_method_state_is_active_public_state'); + + select * from finish(); + +rollback; \ No newline at end of file diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 91b6487e6..46b6b7625 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -341,6 +341,21 @@ "in": "query", "required": false, "type": "string" + }, + { + "name": "list_token", + "description": "An opaque token used to continue an existing iteration or\nrequest updated items. If not specified, pagination\nwill start from the beginning.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "page_size", + "description": "The maximum size of a page in this iteration.\nIf unset, the default page size configured will be used.\nIf the page_size is greater than the max page size configured,\nthe page size will be truncated to this number.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" } ], "tags": [ @@ -7658,7 +7673,36 @@ "items": { "type": "object", "$ref": "#/definitions/controller.api.resources.authmethods.v1.AuthMethod" - } + }, + "description": "The items returned in this page." + }, + "response_type": { + "type": "string", + "description": "The type of response, either \"delta\" or \"complete\".\nDelta signifies that this is part of a paginated result\nor an update to a previously completed pagination.\nComplete signifies that it is the last page." + }, + "list_token": { + "type": "string", + "description": "An opaque token used to continue an existing pagination or\nrequest updated items. Use this token in the next list request\nto request the next page." + }, + "sort_by": { + "type": "string", + "description": "The name of the field which the items are sorted by." + }, + "sort_dir": { + "type": "string", + "description": "The direction of the sort, either \"asc\" or \"desc\"." + }, + "removed_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of item IDs that have been removed since they were returned\nas part of a pagination. They should be dropped from any client cache.\nThis may contain items that are not known to the cache, if they were\ncreated and deleted between listings." + }, + "est_item_count": { + "type": "integer", + "format": "int64", + "description": "An estimate at the total items available. This may change during pagination." } } }, diff --git a/internal/gen/controller/api/services/auth_method_service.pb.go b/internal/gen/controller/api/services/auth_method_service.pb.go index f3cb4a648..b2a9e091d 100644 --- a/internal/gen/controller/api/services/auth_method_service.pb.go +++ b/internal/gen/controller/api/services/auth_method_service.pb.go @@ -133,6 +133,15 @@ type ListAuthMethodsRequest struct { ScopeId string `protobuf:"bytes,1,opt,name=scope_id,proto3" json:"scope_id,omitempty" class:"public" eventstream:"observation"` // @gotags: `class:"public" eventstream:"observation"` Recursive bool `protobuf:"varint,20,opt,name=recursive,proto3" json:"recursive,omitempty" class:"public" eventstream:"observation"` // @gotags: `class:"public" eventstream:"observation"` Filter string `protobuf:"bytes,30,opt,name=filter,proto3" json:"filter,omitempty" class:"public"` // @gotags: `class:"public"` + // An opaque token used to continue an existing iteration or + // request updated items. If not specified, pagination + // will start from the beginning. + ListToken string `protobuf:"bytes,40,opt,name=list_token,proto3" json:"list_token,omitempty" class:"public"` // @gotags: `class:"public"` + // The maximum size of a page in this iteration. + // If unset, the default page size configured will be used. + // If the page_size is greater than the max page size configured, + // the page size will be truncated to this number. + PageSize uint32 `protobuf:"varint,50,opt,name=page_size,proto3" json:"page_size,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *ListAuthMethodsRequest) Reset() { @@ -188,12 +197,47 @@ func (x *ListAuthMethodsRequest) GetFilter() string { return "" } +func (x *ListAuthMethodsRequest) GetListToken() string { + if x != nil { + return x.ListToken + } + return "" +} + +func (x *ListAuthMethodsRequest) GetPageSize() uint32 { + if x != nil { + return x.PageSize + } + return 0 +} + type ListAuthMethodsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + // The items returned in this page. Items []*authmethods.AuthMethod `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + // The type of response, either "delta" or "complete". + // Delta signifies that this is part of a paginated result + // or an update to a previously completed pagination. + // Complete signifies that it is the last page. + ResponseType string `protobuf:"bytes,2,opt,name=response_type,proto3" json:"response_type,omitempty" class:"public"` // @gotags: `class:"public"` + // An opaque token used to continue an existing pagination or + // request updated items. Use this token in the next list request + // to request the next page. + ListToken string `protobuf:"bytes,3,opt,name=list_token,proto3" json:"list_token,omitempty" class:"public"` // @gotags: `class:"public"` + // The name of the field which the items are sorted by. + SortBy string `protobuf:"bytes,4,opt,name=sort_by,proto3" json:"sort_by,omitempty" class:"public"` // @gotags: `class:"public"` + // The direction of the sort, either "asc" or "desc". + SortDir string `protobuf:"bytes,5,opt,name=sort_dir,proto3" json:"sort_dir,omitempty" class:"public"` // @gotags: `class:"public"` + // A list of item IDs that have been removed since they were returned + // as part of a pagination. They should be dropped from any client cache. + // This may contain items that are not known to the cache, if they were + // created and deleted between listings. + RemovedIds []string `protobuf:"bytes,6,rep,name=removed_ids,proto3" json:"removed_ids,omitempty" class:"public"` // @gotags: `class:"public"` + // An estimate at the total items available. This may change during pagination. + EstItemCount uint32 `protobuf:"varint,7,opt,name=est_item_count,proto3" json:"est_item_count,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *ListAuthMethodsResponse) Reset() { @@ -235,6 +279,48 @@ func (x *ListAuthMethodsResponse) GetItems() []*authmethods.AuthMethod { return nil } +func (x *ListAuthMethodsResponse) GetResponseType() string { + if x != nil { + return x.ResponseType + } + return "" +} + +func (x *ListAuthMethodsResponse) GetListToken() string { + if x != nil { + return x.ListToken + } + return "" +} + +func (x *ListAuthMethodsResponse) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +func (x *ListAuthMethodsResponse) GetSortDir() string { + if x != nil { + return x.SortDir + } + return "" +} + +func (x *ListAuthMethodsResponse) GetRemovedIds() []string { + if x != nil { + return x.RemovedIds + } + return nil +} + +func (x *ListAuthMethodsResponse) GetEstItemCount() uint32 { + if x != nil { + return x.EstItemCount + } + return 0 +} + type CreateAuthMethodRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1276,323 +1362,339 @@ var file_controller_api_services_v1_auth_method_service_proto_rawDesc = []byte{ 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x6a, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, - 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, - 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, - 0x65, 0x72, 0x22, 0x64, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xa8, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x1c, 0x0a, + 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, + 0x18, 0x32, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, + 0x65, 0x22, 0xaa, 0x02, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x62, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x75, 0x0a, 0x18, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x47, 0x0a, 0x04, 0x69, 0x74, - 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x22, 0xb0, 0x01, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x47, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x3c, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x63, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x47, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x29, 0x0a, 0x17, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1a, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x19, 0x4f, 0x69, 0x64, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x52, 0x0a, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, - 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa4, 0x02, 0x0a, 0x12, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x0a, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x0b, 0x9a, 0xe3, 0x29, 0x07, 0x64, 0x65, - 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x12, 0x92, 0x01, 0x0a, 0x1c, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x63, 0x68, 0x61, - 0x6e, 0x67, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x42, 0x18, 0x9a, 0xe3, 0x29, 0x04, 0x6f, 0x69, 0x64, 0x63, 0xfa, 0xd2, 0xe4, 0x93, 0x02, - 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x19, 0x6f, - 0x69, 0x64, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, - 0x73, 0x22, 0x5e, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x64, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, + 0x0a, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x18, + 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x72, 0x74, + 0x5f, 0x64, 0x69, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x72, 0x74, + 0x5f, 0x64, 0x69, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x5f, + 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x74, + 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, + 0x65, 0x73, 0x74, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x62, + 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x22, 0x75, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, + 0x12, 0x47, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xb0, 0x01, 0x0a, 0x17, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x47, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x3c, + 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, + 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x63, 0x0a, 0x18, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x22, 0x55, 0x0a, 0x17, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, - 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x96, 0x01, 0x0a, 0x13, 0x4f, 0x69, 0x64, - 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x45, 0x0a, 0x11, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, 0x5f, 0x70, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, - 0x72, 0x75, 0x63, 0x74, 0x52, 0x11, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, 0x5f, - 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x38, 0x0a, 0x18, 0x63, 0x61, 0x63, 0x68, 0x65, - 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, - 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x61, 0x63, 0x68, 0x65, - 0x64, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x22, 0x51, 0x0a, 0x13, 0x4c, 0x64, 0x61, 0x70, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, - 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x6f, - 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x22, 0xed, 0x07, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, - 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x5f, 0x69, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0a, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0a, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x83, 0x01, 0x0a, 0x19, 0x70, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, - 0x41, 0x4c, 0x48, 0x00, 0x52, 0x17, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x77, 0x0a, - 0x15, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x53, 0x74, - 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, - 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, - 0x00, 0x52, 0x13, 0x6f, 0x69, 0x64, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xc9, 0x01, 0x0a, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x5f, - 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, - 0x6b, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x52, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, - 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, - 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x29, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, - 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0xc0, 0x01, 0x0a, 0x2b, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x4f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, - 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x26, 0x6f, - 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x77, 0x0a, 0x15, 0x6c, 0x64, 0x61, 0x70, 0x5f, 0x6c, 0x6f, - 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x64, 0x61, 0x70, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, - 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x13, 0x6c, 0x64, 0x61, 0x70, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x18, - 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, - 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x73, 0x22, 0xf8, 0x06, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, + 0x6d, 0x22, 0x29, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1a, 0x0a, 0x18, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x19, 0x4f, 0x69, 0x64, + 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x52, 0x0a, 0x24, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, + 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x24, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0xa4, 0x02, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x46, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x00, - 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xc3, 0x01, 0x0a, - 0x2c, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x50, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, - 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, - 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x27, 0x6f, 0x69, 0x64, 0x63, 0x41, - 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0xcc, 0x01, 0x0a, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x72, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x53, 0x2e, 0x63, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x0b, + 0x9a, 0xe3, 0x29, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x92, 0x01, 0x0a, 0x1c, 0x6f, 0x69, + 0x64, 0x63, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, + 0x64, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x9a, 0xe3, 0x29, 0x04, 0x6f, 0x69, 0x64, + 0x63, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, + 0x4c, 0x48, 0x00, 0x52, 0x19, 0x6f, 0x69, 0x64, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x07, + 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, 0x5e, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, + 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, - 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x2a, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0xc3, 0x01, 0x0a, 0x2c, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x50, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, - 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x27, - 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x13, 0x61, 0x75, 0x74, 0x68, 0x5f, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, - 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, - 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x11, 0x61, 0x75, 0x74, - 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, - 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x32, - 0x95, 0x0b, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xb8, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, - 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x42, 0x92, 0x41, - 0x1c, 0x12, 0x1a, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, - 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x1d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, - 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, - 0x12, 0xb0, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x73, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x34, 0x92, - 0x41, 0x19, 0x12, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x41, 0x75, - 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x73, 0x12, 0xc5, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x46, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x3a, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, - 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0xc4, 0x01, 0x0a, 0x10, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x92, 0x41, 0x19, - 0x12, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, - 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x3a, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x15, 0x2f, 0x76, 0x31, - 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, - 0x64, 0x7d, 0x12, 0xb6, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x37, 0x92, 0x41, 0x17, 0x12, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, - 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x17, 0x2a, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xcf, 0x01, 0x0a, 0x0b, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5f, 0x92, 0x41, - 0x29, 0x12, 0x27, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x6e, 0x20, 0x4f, 0x49, 0x44, 0x43, 0x20, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2d, - 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x22, 0x2f, 0x76, 0x31, 0x2f, 0x61, - 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, - 0x3a, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2d, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0xf7, 0x01, - 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x2f, + 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x55, 0x0a, 0x17, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x96, + 0x01, 0x0a, 0x13, 0x4f, 0x69, 0x64, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x11, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x74, + 0x72, 0x69, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x11, 0x72, 0x6f, 0x75, 0x6e, + 0x64, 0x74, 0x72, 0x69, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x38, 0x0a, + 0x18, 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, + 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x16, 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, + 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x51, 0x0a, 0x13, 0x4c, 0x64, 0x61, 0x70, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, + 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xed, 0x07, 0x0a, 0x13, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, + 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, + 0x18, 0x01, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, + 0x00, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x83, 0x01, + 0x0a, 0x19, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, + 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, + 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x17, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x12, 0x77, 0x0a, 0x15, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x4f, 0x69, 0x64, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, + 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x13, 0x6f, 0x69, 0x64, 0x63, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xc9, 0x01, 0x0a, + 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, + 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x52, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, + 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, + 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x29, 0x6f, + 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xc0, 0x01, 0x0a, 0x2b, 0x6f, 0x69, 0x64, + 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x4f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x83, 0x01, 0x92, 0x41, 0x47, 0x12, 0x45, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x74, 0x6f, 0x20, - 0x61, 0x6e, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x72, 0x65, 0x74, - 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x33, 0x3a, 0x01, 0x2a, 0x22, 0x2e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, - 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x61, 0x75, 0x74, 0x68, 0x5f, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x75, 0x74, 0x68, 0x65, - 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x42, 0x55, 0xa2, 0xe3, 0x29, 0x04, 0x61, 0x75, 0x74, - 0x68, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, - 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, + 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, + 0x4c, 0x48, 0x00, 0x52, 0x26, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x77, 0x0a, 0x15, 0x6c, + 0x64, 0x61, 0x70, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x64, 0x61, 0x70, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, 0xd2, 0xe4, + 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, + 0x13, 0x6c, 0x64, 0x61, 0x70, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x07, + 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x0b, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0xf8, 0x06, 0x0a, 0x14, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x12, 0xc3, 0x01, 0x0a, 0x2c, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, + 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x50, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, + 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, + 0x27, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xcc, 0x01, 0x0a, 0x2f, 0x6f, 0x69, 0x64, + 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x62, + 0x61, 0x63, 0x6b, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x53, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, + 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, + 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x2a, 0x6f, 0x69, 0x64, + 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xc3, 0x01, 0x0a, 0x2c, 0x6f, 0x69, 0x64, 0x63, + 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, + 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x50, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, + 0x41, 0x4c, 0x48, 0x00, 0x52, 0x27, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, + 0x13, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x10, 0xfa, + 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, + 0x00, 0x52, 0x11, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x07, + 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, + 0x02, 0x10, 0x03, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x32, 0x95, 0x0b, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xb8, 0x01, 0x0a, 0x0d, + 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x30, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, + 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x42, 0x92, 0x41, 0x1c, 0x12, 0x1a, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, + 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, + 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xb0, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x34, 0x92, 0x41, 0x19, 0x12, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, + 0x61, 0x6c, 0x6c, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, + 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, + 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0xc5, 0x01, 0x0a, 0x10, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x46, 0x92, 0x41, 0x1f, 0x12, 0x1d, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, + 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x1e, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, + 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x12, 0xc4, 0x01, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x45, 0x92, 0x41, 0x19, 0x12, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, + 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x32, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xb6, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x37, 0x92, 0x41, 0x17, 0x12, 0x15, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x2a, 0x15, 0x2f, 0x76, 0x31, 0x2f, + 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, + 0x7d, 0x12, 0xcf, 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x5f, 0x92, 0x41, 0x29, 0x12, 0x27, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x6e, + 0x20, 0x4f, 0x49, 0x44, 0x43, 0x20, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2d, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, + 0x22, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2d, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x12, 0xf7, 0x01, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x83, 0x01, 0x92, 0x41, 0x47, 0x12, 0x45, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x75, 0x73, + 0x65, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x6e, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x61, + 0x6e, 0x64, 0x20, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x33, 0x3a, 0x01, 0x2a, 0x22, 0x2e, 0x2f, + 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, + 0x7b, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x7d, + 0x3a, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x42, 0x55, 0xa2, + 0xe3, 0x29, 0x04, 0x61, 0x75, 0x74, 0x68, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/proto/controller/api/services/v1/auth_method_service.proto b/internal/proto/controller/api/services/v1/auth_method_service.proto index b6877cb66..883834da4 100644 --- a/internal/proto/controller/api/services/v1/auth_method_service.proto +++ b/internal/proto/controller/api/services/v1/auth_method_service.proto @@ -107,10 +107,40 @@ message ListAuthMethodsRequest { string scope_id = 1 [json_name = "scope_id"]; // @gotags: `class:"public" eventstream:"observation"` bool recursive = 20 [json_name = "recursive"]; // @gotags: `class:"public" eventstream:"observation"` string filter = 30 [json_name = "filter"]; // @gotags: `class:"public"` + // An opaque token used to continue an existing iteration or + // request updated items. If not specified, pagination + // will start from the beginning. + string list_token = 40 [json_name = "list_token"]; // @gotags: `class:"public"` + // The maximum size of a page in this iteration. + // If unset, the default page size configured will be used. + // If the page_size is greater than the max page size configured, + // the page size will be truncated to this number. + uint32 page_size = 50 [json_name = "page_size"]; // @gotags: `class:"public"` } message ListAuthMethodsResponse { + // The items returned in this page. repeated resources.authmethods.v1.AuthMethod items = 1; + // The type of response, either "delta" or "complete". + // Delta signifies that this is part of a paginated result + // or an update to a previously completed pagination. + // Complete signifies that it is the last page. + string response_type = 2 [json_name = "response_type"]; // @gotags: `class:"public"` + // An opaque token used to continue an existing pagination or + // request updated items. Use this token in the next list request + // to request the next page. + string list_token = 3 [json_name = "list_token"]; // @gotags: `class:"public"` + // The name of the field which the items are sorted by. + string sort_by = 4 [json_name = "sort_by"]; // @gotags: `class:"public"` + // The direction of the sort, either "asc" or "desc". + string sort_dir = 5 [json_name = "sort_dir"]; // @gotags: `class:"public"` + // A list of item IDs that have been removed since they were returned + // as part of a pagination. They should be dropped from any client cache. + // This may contain items that are not known to the cache, if they were + // created and deleted between listings. + repeated string removed_ids = 6 [json_name = "removed_ids"]; // @gotags: `class:"public"` + // An estimate at the total items available. This may change during pagination. + uint32 est_item_count = 7 [json_name = "est_item_count"]; // @gotags: `class:"public"` } message CreateAuthMethodRequest { diff --git a/internal/tests/api/authmethods/authmethod_test.go b/internal/tests/api/authmethods/authmethod_test.go index dc585dac6..2699189da 100644 --- a/internal/tests/api/authmethods/authmethod_test.go +++ b/internal/tests/api/authmethods/authmethod_test.go @@ -200,9 +200,9 @@ func TestList(t *testing.T) { result, err := amClient.List(tc.Context(), global) require.NoError(err) require.Len(result.Items, 3) - genOIDCAM := result.Items[0] - genPWAM := result.Items[1] - genLDAPAM := result.Items[2] + genLDAPAM := result.Items[0] + genOIDCAM := result.Items[1] + genPWAM := result.Items[2] pwAM, err := amClient.Create(tc.Context(), "password", global, authmethods.WithName("bar"),