From a3f70262613fd14e2ece548347814aff09bcacc7 Mon Sep 17 00:00:00 2001 From: Todd Date: Tue, 2 Apr 2024 00:01:15 +0000 Subject: [PATCH 1/6] backport of commit 3c0c68d3efc3b0074c0ca5b21039263d68e74687 --- .../repository_alias_list_resolvable.go | 220 +++++ internal/alias/target/service_list.go | 44 + internal/alias/target/service_list_page.go | 59 ++ internal/alias/target/service_list_refresh.go | 64 ++ .../alias/target/service_list_refresh_page.go | 72 ++ .../service_list_resolvable_ext_test.go | 738 ++++++++++++++++ internal/cmd/base/initial_resources.go | 1 + internal/daemon/controller/auth/auth.go | 52 +- internal/daemon/controller/handler.go | 2 +- .../controller/handlers/users/user_service.go | 225 ++++- .../handlers/users/user_service_test.go | 432 +++++++++- .../daemon/controller/rate_limiter_test.go | 58 +- .../defaults.json | 28 +- .../max_size.json | 26 + .../override.json | 28 +- internal/gen/controller.swagger.json | 87 ++ .../api/services/user_service.pb.go | 797 ++++++++++++------ .../api/services/user_service.pb.gw.go | 121 +++ .../api/services/user_service_grpc.pb.go | 59 +- internal/iam/repository_role_grant.go | 2 +- internal/perms/acl.go | 114 ++- internal/perms/acl_test.go | 363 +++++++- internal/perms/grants.go | 54 ++ .../api/services/v1/user_service.proto | 54 ++ internal/types/action/action.go | 3 + internal/types/action/action_test.go | 32 + 26 files changed, 3299 insertions(+), 436 deletions(-) create mode 100644 internal/alias/target/repository_alias_list_resolvable.go create mode 100644 internal/alias/target/service_list_resolvable_ext_test.go diff --git a/internal/alias/target/repository_alias_list_resolvable.go b/internal/alias/target/repository_alias_list_resolvable.go new file mode 100644 index 0000000000..da78507eaf --- /dev/null +++ b/internal/alias/target/repository_alias_list_resolvable.go @@ -0,0 +1,220 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package target + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/perms" +) + +// targetAndScopeIdsForDestinations returns the target ids for which there is +// at least one permission. If all targets in a specific scope are granted +// permission for an action, then the scope id is in the returned scope id slice. +func targetAndScopeIdsForDestinations(perms []perms.Permission) ([]string, []string) { + var targetIds, scopeIds []string + for _, perm := range perms { + switch { + case perm.All: + scopeIds = append(scopeIds, perm.ScopeId) + case len(perm.ResourceIds) > 0: + targetIds = append(targetIds, perm.ResourceIds...) + } + } + return targetIds, scopeIds +} + +// listResolvableAliases lists aliases which have a destination id set to that +// of a target for which there is permission in the provided slice of permissions. +// Only WithLimit and WithStartPageAfterItem options are supported. +func (r *Repository) listResolvableAliases(ctx context.Context, permissions []perms.Permission, opt ...Option) ([]*Alias, time.Time, error) { + const op = "target.(Repository).listResolvableAliases" + switch { + case len(permissions) == 0: + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions") + } + toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions) + + opts, err := getOpts(opt...) + if err != nil { + return nil, time.Time{}, errors.Wrap(ctx, err, op) + } + + limit := r.defaultLimit + switch { + case opts.withLimit > 0: + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + case opts.withLimit < 0: + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "limit must be non-negative") + } + + var args []any + var destinationIdClauses []string + if len(toTargetIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id in @target_ids") + args = append(args, sql.Named("target_ids", toTargetIds)) + } + if len(toTargetsInScopeIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target where project_id in @target_scope_ids)") + args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds)) + } + if len(destinationIdClauses) == 0 { + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + } + + whereClause := fmt.Sprintf("destination_id is not null and (%s)", strings.Join(destinationIdClauses, " or ")) + + if opts.withStartPageAfterItem != nil { + whereClause = fmt.Sprintf("(create_time, public_id) < (@last_item_create_time, @last_item_id) and %s", whereClause) + args = append(args, + sql.Named("last_item_create_time", opts.withStartPageAfterItem.GetCreateTime()), + sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()), + ) + } + dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("create_time desc, public_id desc"), db.WithDebug(true)} + return r.queryAliases(ctx, whereClause, args, dbOpts...) +} + +// listResolvableAliasesRefresh lists aliases limited by the list +// permissions of the repository. +// Supported options: +// - withLimit +// - withStartPageAfterItem +func (r *Repository) listResolvableAliasesRefresh(ctx context.Context, updatedAfter time.Time, permissions []perms.Permission, opt ...Option) ([]*Alias, time.Time, error) { + const op = "target.(Repository).listResolvableAliasesRefresh" + + switch { + case updatedAfter.IsZero(): + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updated after time") + case len(permissions) == 0: + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions") + } + toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions) + + opts, err := getOpts(opt...) + if err != nil { + return nil, time.Time{}, errors.Wrap(ctx, err, op) + } + + limit := r.defaultLimit + switch { + case opts.withLimit > 0: + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + case opts.withLimit < 0: + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "limit must be non-negative") + } + + var args []any + var destinationIdClauses []string + if len(toTargetIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id in @target_ids") + args = append(args, sql.Named("target_ids", toTargetIds)) + } + if len(toTargetsInScopeIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target where project_id in @target_scope_ids)") + args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds)) + } + if len(destinationIdClauses) == 0 { + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + } + + whereClause := fmt.Sprintf("update_time > @updated_after_time and destination_id is not null and (%s)", + strings.Join(destinationIdClauses, " or ")) + args = append(args, + sql.Named("updated_after_time", timestamp.New(updatedAfter)), + ) + if opts.withStartPageAfterItem != nil { + whereClause = fmt.Sprintf("(update_time, public_id) < (@last_item_update_time, @last_item_id) and %s", whereClause) + args = append(args, + sql.Named("last_item_update_time", opts.withStartPageAfterItem.GetUpdateTime()), + sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()), + ) + } + + dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("update_time desc, public_id desc")} + return r.queryAliases(ctx, whereClause, args, dbOpts...) +} + +// listRemovedResolvableIds lists the public IDs of any aliases deleted since +// the timestamp provided or which have been updated since the timestamp provided +// and do not have a destination id set to the id of a target for which there +// are permissions in the provided slice of permissions. +func (r *Repository) listRemovedResolvableAliasIds(ctx context.Context, since time.Time, permissions []perms.Permission) ([]string, time.Time, error) { + const op = "target.(Repository).listRemovedResolvableIds" + switch { + case len(permissions) == 0: + // while a lack of permissions is one way for targets to not be included + // in the list of resolvable aliases, if permissions were always empty + // then no aliases would have been returned in the first place and so + // no ids would need to be removed. If permissions were changed to + // become empty, then the list token would be invalidated and we shouldnt + // have made it here, so it is an error for an empty slice of permissions + // to be provided. + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions") + } + toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions) + + var args []any + var destinationIdClauses []string + if len(toTargetIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id not in @target_ids") + args = append(args, sql.Named("target_ids", toTargetIds)) + } + if len(toTargetsInScopeIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id not in (select public_id from target where project_id in @target_scope_ids)") + args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds)) + } + if len(destinationIdClauses) == 0 { + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + } + whereClause := fmt.Sprintf("update_time > @updated_after_time and (destination_id is null or (%s))", + strings.Join(destinationIdClauses, " and ")) + args = append(args, + sql.Named("updated_after_time", timestamp.New(since)), + ) + + // The calculating of the deleted aliases and the non matching alises + // must happen in the same transaction to ensure consistency. + var notMatchingAliases []*Alias + var deletedAliases []*deletedAlias + var transactionTimestamp time.Time + if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, _ db.Writer) error { + if err := r.SearchWhere(ctx, &deletedAliases, "delete_time >= ?", []any{since}); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted aliases")) + } + + var inRet []*Alias + if err := r.SearchWhere(ctx, &inRet, whereClause, args); err != nil { + return errors.Wrap(ctx, err, op) + } + notMatchingAliases = inRet + + var err error + transactionTimestamp, err = r.Now(ctx) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to get transaction timestamp")) + } + + return nil + }); err != nil { + return nil, time.Time{}, err + } + var aliasIds []string + for _, da := range deletedAliases { + aliasIds = append(aliasIds, da.PublicId) + } + for _, na := range notMatchingAliases { + aliasIds = append(aliasIds, na.PublicId) + } + return aliasIds, transactionTimestamp, nil +} diff --git a/internal/alias/target/service_list.go b/internal/alias/target/service_list.go index 19f808f259..5061f1b12e 100644 --- a/internal/alias/target/service_list.go +++ b/internal/alias/target/service_list.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/pagination" + "github.com/hashicorp/boundary/internal/perms" ) // ListAliases lists up to page size aliases, filtering out entries that @@ -51,3 +52,46 @@ func ListAliases( return pagination.List(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount) } + +// ListResolvableAliases lists up to page size aliases, filtering out entries that +// do not pass the filter item function. It will automatically request +// more aliases from the database, at page size chunks, to fill the page. Only +// aliases which have the destination id set to a target for which there are +// permissions in the provided slice will be returned. +// It returns a new list token used to continue pagination or refresh items. +// Aliases are ordered by create time descending (most recently created first). +func ListResolvableAliases( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn pagination.ListFilterFunc[*Alias], + repo *Repository, + permissions []perms.Permission, +) (*pagination.ListResponse[*Alias], error) { + const op = "target.ListResolvableAliases" + + 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(permissions) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target permissions") + } + + listItemsFn := func(ctx context.Context, lastPageItem *Alias, limit int) ([]*Alias, time.Time, error) { + opts := []Option{ + WithLimit(limit), + } + if lastPageItem != nil { + opts = append(opts, WithStartPageAfterItem(lastPageItem)) + } + return repo.listResolvableAliases(ctx, permissions, opts...) + } + + return pagination.List(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount) +} diff --git a/internal/alias/target/service_list_page.go b/internal/alias/target/service_list_page.go index cb5d70d248..a21f22b6cc 100644 --- a/internal/alias/target/service_list_page.go +++ b/internal/alias/target/service_list_page.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/listtoken" "github.com/hashicorp/boundary/internal/pagination" + "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/types/resource" ) @@ -68,3 +69,61 @@ func ListAliasesPage( return pagination.ListPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, tok) } + +// ListResolvableAliasesPage lists up to page size aliases, filtering out entries that +// do not pass the filter item function. It will automatically request +// more aliases from the database, at page size chunks, to fill the page. +// Only aliases which resolve to a target for which there are permissions in the +// included slice of permissions are returned. +// 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. +// Aliases are ordered by create time descending (most recently created first). +func ListResolvableAliasesPage( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn pagination.ListFilterFunc[*Alias], + tok *listtoken.Token, + repo *Repository, + perms []perms.Permission, +) (*pagination.ListResponse[*Alias], error) { + const op = "target.ListResolvableAliasesPage" + + 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 tok.ResourceType != resource.Alias: + return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a alias resource type") + case len(perms) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing permissions") + } + 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 *Alias, limit int) ([]*Alias, time.Time, error) { + opts := []Option{ + WithLimit(limit), + } + if lastPageItem != nil { + opts = append(opts, WithStartPageAfterItem(lastPageItem)) + } else { + lastItem, err := tok.LastItem(ctx) + if err != nil { + return nil, time.Time{}, err + } + opts = append(opts, WithStartPageAfterItem(lastItem)) + } + return repo.listResolvableAliases(ctx, perms, opts...) + } + + return pagination.ListPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, tok) +} diff --git a/internal/alias/target/service_list_refresh.go b/internal/alias/target/service_list_refresh.go index 67ae7f43eb..dff67f517c 100644 --- a/internal/alias/target/service_list_refresh.go +++ b/internal/alias/target/service_list_refresh.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/listtoken" "github.com/hashicorp/boundary/internal/pagination" + "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/types/resource" ) @@ -74,3 +75,66 @@ func ListAliasesRefresh( return pagination.ListRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, listDeletedIdsFn, tok) } + +// ListResolvableAliasesRefresh lists up to page size aliases, filtering out entries that +// do not pass the filter item function. It will automatically request +// more aliases 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. +// Aliases are ordered by update time descending (most recently updated first). +// Aliases may contain items that were already returned during the initial +// pagination phase. It also returns a list of any aliases deleted since the +// start of the initial pagination phase or last response, or which have been +// updated since that last time and do not have a destination id that is for +// a target that is included in the list of permissions. +func ListResolvableAliasesRefresh( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn pagination.ListFilterFunc[*Alias], + tok *listtoken.Token, + repo *Repository, + permissions []perms.Permission, +) (*pagination.ListResponse[*Alias], error) { + const op = "target.ListResolvableAliasesRefresh" + + 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 tok.ResourceType != resource.Alias: + return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a alias resource type") + case len(permissions) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target permissions") + } + 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 *Alias, limit int) ([]*Alias, time.Time, error) { + opts := []Option{ + WithLimit(limit), + } + if lastPageItem != nil { + opts = append(opts, WithStartPageAfterItem(lastPageItem)) + } + // Add the database read timeout to account for any creations missed due to concurrent + // transactions in the initial pagination phase. + return repo.listResolvableAliasesRefresh(ctx, rt.PreviousPhaseUpperBound.Add(-globals.RefreshReadLookbackDuration), permissions, 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. + return repo.listRemovedResolvableAliasIds(ctx, since.Add(-globals.RefreshReadLookbackDuration), permissions) + } + + return pagination.ListRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, listDeletedIdsFn, tok) +} diff --git a/internal/alias/target/service_list_refresh_page.go b/internal/alias/target/service_list_refresh_page.go index 417cbf0363..cb15af563b 100644 --- a/internal/alias/target/service_list_refresh_page.go +++ b/internal/alias/target/service_list_refresh_page.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/listtoken" "github.com/hashicorp/boundary/internal/pagination" + "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/types/resource" ) @@ -81,3 +82,74 @@ func ListAliasesRefreshPage( return pagination.ListRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, listDeletedIdsFn, tok) } + +// ListResolvableAliasesRefreshPage lists up to page size aliases, filtering out entries that +// do not pass the filter item function. It will automatically request +// more aliases from the database, at page size chunks, to fill the page. +// It will start its paging based on the information in the token. +// Only aliases which resolve to a target for which there are permissions in the +// included slice of permissions are returned. +// It returns a new list token used to continue pagination or refresh items. +// Aliases are ordered by update time descending (most recently updated first). +// Aliases may contain items that were already returned during the initial +// pagination phase. It also returns a list of any aliases deleted since the +// last response or which were updated since the last response and do not resolve +// to a target for which there are permissions in the included slice of permissions. +func ListResolvableAliasesRefreshPage( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn pagination.ListFilterFunc[*Alias], + tok *listtoken.Token, + repo *Repository, + permissions []perms.Permission, +) (*pagination.ListResponse[*Alias], error) { + const op = "target.ListResolvableAliasesRefreshPage" + + 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 tok.ResourceType != resource.Alias: + return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a alias resource type") + case len(permissions) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target permissions") + } + 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 *Alias, limit int) ([]*Alias, time.Time, error) { + opts := []Option{ + WithLimit(limit), + } + if lastPageItem != nil { + opts = append(opts, WithStartPageAfterItem(lastPageItem)) + } else { + lastItem, err := tok.LastItem(ctx) + if err != nil { + return nil, time.Time{}, err + } + opts = append(opts, WithStartPageAfterItem(lastItem)) + } + // Add the database read timeout to account for any creations missed due to concurrent + // transactions in the original list pagination phase. + return repo.listResolvableAliasesRefresh(ctx, rt.PhaseLowerBound.Add(-globals.RefreshReadLookbackDuration), permissions, 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.listRemovedResolvableAliasIds(ctx, since.Add(-globals.RefreshReadLookbackDuration), permissions) + } + + return pagination.ListRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, listDeletedIdsFn, tok) +} diff --git a/internal/alias/target/service_list_resolvable_ext_test.go b/internal/alias/target/service_list_resolvable_ext_test.go new file mode 100644 index 0000000000..4e03911b56 --- /dev/null +++ b/internal/alias/target/service_list_resolvable_ext_test.go @@ -0,0 +1,738 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package target_test + +import ( + "context" + "crypto/rand" + "fmt" + "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/alias/target" + "github.com/hashicorp/boundary/internal/alias/target/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/perms" + "github.com/hashicorp/boundary/internal/target/tcp" + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestService_ListResolvableAliases(t *testing.T) { + fiveDaysAgo := time.Now() + // Set database read timeout to avoid duplicates in response + oldReadTimeout := globals.RefreshReadLookbackDuration + globals.RefreshReadLookbackDuration = 0 + t.Cleanup(func() { + globals.RefreshReadLookbackDuration = oldReadTimeout + }) + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + sqlDB, err := conn.SqlDB(context.Background()) + require.NoError(t, err) + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + require.NoError(t, kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + tar := tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "target1") + + var byIdResources []*target.Alias + for i := 0; i < 5; i++ { + r := target.TestAlias(t, rw, fmt.Sprintf("test%d.alias.by-id", i), target.WithDestinationId(tar.GetPublicId())) + byIdResources = append(byIdResources, r) + } + byIdPerms := []perms.Permission{ + { + ScopeId: proj.GetPublicId(), + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{tar.GetPublicId(), "ttcp_unknownid"}, + OnlySelf: false, + All: false, + }, + } + // Reverse since we read items in descending order (newest first) + slices.Reverse(byIdResources) + + _, proj2 := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + tar2 := tcp.TestTarget(ctx, t, conn, proj2.GetPublicId(), "target2") + var byScopeResources []*target.Alias + for i := 0; i < 5; i++ { + r := target.TestAlias(t, rw, fmt.Sprintf("test%d.alias.by-scope", i), target.WithDestinationId(tar2.GetPublicId())) + byScopeResources = append(byScopeResources, r) + } + byScopePerms := []perms.Permission{ + { + ScopeId: proj2.GetPublicId(), + Resource: resource.Target, + Action: action.ListResolvableAliases, + OnlySelf: false, + All: true, + }, + } + // Reverse since we read items in descending order (newest first) + slices.Reverse(byScopeResources) + + repo, err := target.NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, err) + + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + cmpIgnoreUnexportedOpts := cmpopts.IgnoreUnexported(target.Alias{}, store.Alias{}, timestamp.Timestamp{}, timestamppb.Timestamp{}) + + t.Run("List validation", func(t *testing.T) { + t.Parallel() + t.Run("missing grants hash", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + _, err := target.ListResolvableAliases(ctx, nil, 1, filterFunc, repo, byIdPerms) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 0, filterFunc, repo, byIdPerms) + 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, r *target.Alias) (bool, error) { + return true, nil + } + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), -1, filterFunc, repo, byIdPerms) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("nil filter func", func(t *testing.T) { + t.Parallel() + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) + require.ErrorContains(t, err, "missing filter item callback") + }) + t.Run("nil repo", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, nil, byIdPerms) + require.ErrorContains(t, err, "missing repo") + }) + t.Run("missing target permissions", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, nil) + require.ErrorContains(t, err, "missing target permissions") + }) + }) + 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, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesPage(ctx, nil, 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, byIdPerms) + 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, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, byIdPerms) + 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.Alias, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, nil, tok, repo, byIdPerms) + require.ErrorContains(t, err, "missing filter item callback") + }) + t.Run("nil token", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + _, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, byIdPerms) + require.ErrorContains(t, err, "missing token") + }) + t.Run("wrong token type", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "token did not have a pagination token component") + }) + t.Run("nil repo", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, byIdPerms) + require.ErrorContains(t, err, "missing repo") + }) + t.Run("missing permissions", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil) + require.ErrorContains(t, err, "missing permissions") + }) + t.Run("wrong token resource type", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "token did not have a alias 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, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefresh(ctx, nil, 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 0, filterFunc, tok, repo, byIdPerms) + 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, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), -1, filterFunc, tok, repo, byIdPerms) + 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.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, nil, tok, repo, byIdPerms) + require.ErrorContains(t, err, "missing filter item callback") + }) + t.Run("nil token", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + _, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, nil, repo, byIdPerms) + require.ErrorContains(t, err, "missing token") + }) + t.Run("wrong token type", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "token did not have a start-refresh token component") + }) + t.Run("nil repo", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, nil, byIdPerms) + require.ErrorContains(t, err, "missing repo") + }) + t.Run("missing permissions", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil) + require.ErrorContains(t, err, "missing target permissions") + }) + t.Run("wrong token resource type", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "token did not have a alias 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, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefreshPage(ctx, nil, 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, byIdPerms) + 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, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, byIdPerms) + 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.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, nil, tok, repo, byIdPerms) + require.ErrorContains(t, err, "missing filter item callback") + }) + t.Run("nil token", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, byIdPerms) + require.ErrorContains(t, err, "missing token") + }) + t.Run("wrong token type", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "token did not have a refresh token component") + }) + t.Run("nil repo", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, byIdPerms) + require.ErrorContains(t, err, "missing repo") + }) + t.Run("missing permissions", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) + require.NoError(t, err) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil) + require.ErrorContains(t, err, "missing target permissions") + }) + t.Run("wrong token resource type", func(t *testing.T) { + t.Parallel() + filterFunc := func(_ context.Context, r *target.Alias) (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 = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + require.ErrorContains(t, err, "token did not have a alias resource type") + }) + }) + + t.Run("simple pagination", func(t *testing.T) { + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + + cases := []struct { + name string + perms []perms.Permission + resourceSlice []*target.Alias + }{ + { + name: "by-id", + perms: byIdPerms, + resourceSlice: byIdResources, + }, + { + name: "by-scope", + perms: byScopePerms, + resourceSlice: byScopeResources, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, tc.perms) + 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, 10) + require.Empty(t, resp.DeletedIds) + require.Len(t, resp.Items, 1) + require.Empty(t, cmp.Diff(resp.Items[0], tc.resourceSlice[0], cmpIgnoreUnexportedOpts), "resources did not match", tc.resourceSlice, "resp", resp.Items) + + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, tc.perms) + require.NoError(t, err) + require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp2.CompleteListing) + require.Equal(t, resp2.EstimatedItemCount, 10) + require.Empty(t, resp2.DeletedIds) + require.Len(t, resp2.Items, 1) + require.Empty(t, cmp.Diff(resp2.Items[0], tc.resourceSlice[1], cmpIgnoreUnexportedOpts)) + + resp3, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, tc.perms) + require.NoError(t, err) + require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp3.CompleteListing) + require.Equal(t, resp3.EstimatedItemCount, 10) + require.Empty(t, resp3.DeletedIds) + require.Len(t, resp3.Items, 1) + require.Empty(t, cmp.Diff(resp3.Items[0], tc.resourceSlice[2], cmpIgnoreUnexportedOpts)) + + resp4, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, tc.perms) + require.NoError(t, err) + require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp4.CompleteListing) + require.Equal(t, resp4.EstimatedItemCount, 10) + require.Empty(t, resp4.DeletedIds) + require.Len(t, resp4.Items, 1) + require.Empty(t, cmp.Diff(resp4.Items[0], tc.resourceSlice[3], cmpIgnoreUnexportedOpts)) + + resp5, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, tc.perms) + require.NoError(t, err) + require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp5.CompleteListing) + require.Equal(t, resp5.EstimatedItemCount, 10) + require.Empty(t, resp5.DeletedIds) + require.Len(t, resp5.Items, 1) + require.Empty(t, cmp.Diff(resp5.Items[0], tc.resourceSlice[4], cmpIgnoreUnexportedOpts)) + + // Finished initial pagination phase, request refresh + // Expect no results. + resp6, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp5.ListToken, repo, tc.perms) + require.NoError(t, err) + require.Equal(t, resp6.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp6.CompleteListing) + require.Equal(t, resp6.EstimatedItemCount, 10) + require.Empty(t, resp6.DeletedIds) + require.Empty(t, resp6.Items) + + // Create some new aliases + newR1 := target.TestAlias(t, rw, "first.new.alias", target.WithDestinationId(tc.resourceSlice[0].GetDestinationId())) + newR2 := target.TestAlias(t, rw, "second.new.alias", target.WithDestinationId(tc.resourceSlice[0].GetDestinationId())) + t.Cleanup(func() { + _, err = repo.DeleteAlias(ctx, newR1.GetPublicId()) + require.NoError(t, err) + _, err = repo.DeleteAlias(ctx, newR2.GetPublicId()) + 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 newR2 + resp7, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp6.ListToken, repo, tc.perms) + require.NoError(t, err) + require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp7.CompleteListing) + require.Equal(t, resp7.EstimatedItemCount, 12) + require.Empty(t, resp7.DeletedIds) + require.Len(t, resp7.Items, 1) + require.Empty(t, cmp.Diff(resp7.Items[0], newR2, cmpIgnoreUnexportedOpts)) + + // Refresh again, should get newR1 + resp8, err := target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp7.ListToken, repo, tc.perms) + require.NoError(t, err) + require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp8.CompleteListing) + require.Equal(t, resp8.EstimatedItemCount, 12) + require.Empty(t, resp8.DeletedIds) + require.Len(t, resp8.Items, 1) + require.Empty(t, cmp.Diff(resp8.Items[0], newR1, cmpIgnoreUnexportedOpts)) + + // Refresh again, should get no results + resp9, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp8.ListToken, repo, tc.perms) + require.NoError(t, err) + require.Equal(t, resp9.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp9.CompleteListing) + require.Equal(t, resp9.EstimatedItemCount, 12) + require.Empty(t, resp9.DeletedIds) + require.Empty(t, resp9.Items) + }) + } + }) + + t.Run("simple pagination with aggressive filtering", func(t *testing.T) { + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return r.GetPublicId() == byIdResources[1].GetPublicId() || + r.GetPublicId() == byIdResources[len(byIdResources)-1].GetPublicId(), nil + } + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, byIdPerms) + 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, 10) + require.Empty(t, resp.DeletedIds) + require.Len(t, resp.Items, 1) + require.Empty(t, cmp.Diff(resp.Items[0], byIdResources[1], cmpIgnoreUnexportedOpts)) + + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, byIdPerms) + 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, 10) + require.Empty(t, resp2.DeletedIds) + require.Len(t, resp2.Items, 1) + require.Empty(t, cmp.Diff(resp2.Items[0], byIdResources[len(byIdResources)-1], cmpIgnoreUnexportedOpts)) + + // request a refresh, nothing should be returned + resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, byIdPerms) + require.NoError(t, err) + require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp3.CompleteListing) + require.Equal(t, resp3.EstimatedItemCount, 10) + require.Empty(t, resp3.DeletedIds) + require.Empty(t, resp3.Items) + + // Create some new aliases + newR1 := target.TestAlias(t, rw, "new.alias.one", target.WithDestinationId(tar.GetPublicId())) + newR2 := target.TestAlias(t, rw, "new.alias.two", target.WithDestinationId(tar.GetPublicId())) + newR3 := target.TestAlias(t, rw, "new.alias.three", target.WithDestinationId(tar.GetPublicId())) + newR4 := target.TestAlias(t, rw, "new.alias.four", target.WithDestinationId(tar.GetPublicId())) + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + t.Cleanup(func() { + _, err = repo.DeleteAlias(ctx, newR1.GetPublicId()) + require.NoError(t, err) + _, err = repo.DeleteAlias(ctx, newR2.GetPublicId()) + require.NoError(t, err) + _, err = repo.DeleteAlias(ctx, newR3.GetPublicId()) + require.NoError(t, err) + _, err = repo.DeleteAlias(ctx, newR4.GetPublicId()) + require.NoError(t, err) + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + }) + + filterFunc = func(_ context.Context, r *target.Alias) (bool, error) { + return r.GetPublicId() == newR3.GetPublicId() || + r.GetPublicId() == newR1.GetPublicId(), nil + } + // Refresh again, should get newR3 + resp4, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, byIdPerms) + require.NoError(t, err) + require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp4.CompleteListing) + require.Equal(t, resp4.EstimatedItemCount, 14) + require.Empty(t, resp4.DeletedIds) + require.Len(t, resp4.Items, 1) + require.Empty(t, cmp.Diff(resp4.Items[0], newR3, cmpIgnoreUnexportedOpts)) + + // Refresh again, should get newR1 + resp5, err := target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, byIdPerms) + require.NoError(t, err) + require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp5.CompleteListing) + require.Equal(t, resp5.EstimatedItemCount, 14) + require.Empty(t, resp5.DeletedIds) + require.Len(t, resp5.Items, 1) + require.Empty(t, cmp.Diff(resp5.Items[0], newR1, cmpIgnoreUnexportedOpts)) + }) + + t.Run("simple pagination with destination id changes", func(t *testing.T) { + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + firstUpdatedA := byScopeResources[0] + // this no longer has the destination id that has permissions + firstUpdatedA.DestinationId = tar.GetPublicId() + firstUpdatedA, _, err = repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) + require.NoError(t, err) + byScopeResources = byScopeResources[1:] + t.Cleanup(func() { + firstUpdatedA.DestinationId = tar2.GetPublicId() + firstUpdatedA, _, err = repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) + require.NoError(t, err) + byScopeResources = append([]*target.Alias{firstUpdatedA}, byScopeResources...) + }) + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, byScopePerms) + 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, 10) + require.Empty(t, resp.DeletedIds) + require.Len(t, resp.Items, 1) + require.Empty(t, cmp.Diff(resp.Items[0], byScopeResources[0], cmpIgnoreUnexportedOpts)) + + // request remaining results + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 3, filterFunc, resp.ListToken, repo, byScopePerms) + require.NoError(t, err) + require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp2.CompleteListing) + require.Equal(t, resp2.EstimatedItemCount, 10) + require.Empty(t, resp2.DeletedIds) + require.Len(t, resp2.Items, 3) + require.Empty(t, cmp.Diff(resp2.Items, byScopeResources[1:], cmpIgnoreUnexportedOpts)) + + secondA := byScopeResources[0] + // this no longer has the destination id that has permissions + secondA.DestinationId = tar.GetPublicId() + secondA, _, err = repo.UpdateAlias(ctx, secondA, secondA.GetVersion(), []string{"DestinationId"}) + require.NoError(t, err) + byScopeResources = byScopeResources[1:] + t.Cleanup(func() { + secondA.DestinationId = tar2.GetPublicId() + secondA, _, err = repo.UpdateAlias(ctx, secondA, secondA.GetVersion(), []string{"DestinationId"}) + require.NoError(t, err) + byScopeResources = append([]*target.Alias{secondA}, byScopeResources...) + }) + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // request a refresh, nothing should be returned except the deleted id + resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, byScopePerms) + require.NoError(t, err) + require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp3.CompleteListing) + require.Equal(t, resp3.EstimatedItemCount, 10) + require.Contains(t, resp3.DeletedIds, secondA.GetPublicId()) + require.Empty(t, resp3.Items) + }) + + t.Run("simple pagination with deletion", func(t *testing.T) { + filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { + return true, nil + } + deletedAliasId := byIdResources[0].GetPublicId() + _, err := repo.DeleteAlias(ctx, deletedAliasId) + require.NoError(t, err) + byIdResources = byIdResources[1:] + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, byIdPerms) + 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, 9) + require.Empty(t, resp.DeletedIds) + require.Len(t, resp.Items, 1) + require.Empty(t, cmp.Diff(resp.Items[0], byIdResources[0], cmpIgnoreUnexportedOpts)) + + // request remaining results + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 8, filterFunc, resp.ListToken, repo, byIdPerms) + require.NoError(t, err) + require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp2.CompleteListing) + require.Equal(t, resp2.EstimatedItemCount, 9) + require.Empty(t, resp2.DeletedIds) + require.Len(t, resp2.Items, 3) + require.Empty(t, cmp.Diff(resp2.Items, byIdResources[1:], cmpIgnoreUnexportedOpts)) + + deletedAliasId = byIdResources[0].GetPublicId() + _, err = repo.DeleteAlias(ctx, deletedAliasId) + require.NoError(t, err) + byIdResources = byIdResources[1:] + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // request a refresh, nothing should be returned except the deleted id + resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, byIdPerms) + require.NoError(t, err) + require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp3.CompleteListing) + require.Equal(t, resp3.EstimatedItemCount, 8) + require.Contains(t, resp3.DeletedIds, deletedAliasId) + require.Empty(t, resp3.Items) + }) +} diff --git a/internal/cmd/base/initial_resources.go b/internal/cmd/base/initial_resources.go index d559281e7c..87dc30a106 100644 --- a/internal/cmd/base/initial_resources.go +++ b/internal/cmd/base/initial_resources.go @@ -114,6 +114,7 @@ func (b *Server) CreateInitialAuthenticatedUserRole(ctx context.Context, opt ... "ids=*;type=auth-token;actions=list", "ids={{.Account.Id}};actions=read,change-password", "ids=*;type=session;actions=list,read:self,cancel:self", + "ids={{.User.Id}};type=user;actions=list-resolvable-aliases", } opts := GetOpts(opt...) if opts.withAuthUserTargetAuthorizeSessionGrant { diff --git a/internal/daemon/controller/auth/auth.go b/internal/daemon/controller/auth/auth.go index 7689ce5cb2..869f83564b 100644 --- a/internal/daemon/controller/auth/auth.go +++ b/internal/daemon/controller/auth/auth.go @@ -5,13 +5,9 @@ package auth import ( "context" - "encoding/binary" stderrors "errors" "fmt" - "hash" - "hash/fnv" "net/http" - "slices" "strings" "time" @@ -102,7 +98,7 @@ type VerifyResults struct { v *verifier // Used to generate a hash of all grants - grants []perms.GrantTuple + grants perms.GrantTuples } type verifier struct { @@ -947,49 +943,5 @@ func (r *VerifyResults) ScopesAuthorizedForList(ctx context.Context, rootScopeId // GrantsHash returns a stable hash of all the grants in the verify results. func (r *VerifyResults) GrantsHash(ctx context.Context) ([]byte, error) { - const op = "auth.GrantsHash" - var values []string - for _, grant := range r.grants { - values = append(values, grant.Grant, grant.RoleId, grant.ScopeId) - } - // Sort for deterministic output - slices.Sort(values) - hashVal, err := hashStrings(values...) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - return binary.LittleEndian.AppendUint64(make([]byte, 0, 4), hashVal), nil -} - -func hashStrings(s ...string) (uint64, error) { - hasher := fnv.New64() - var h uint64 - var err error - for _, current := range s { - hasher.Reset() - if _, err = hasher.Write([]byte(current)); err != nil { - return 0, err - } - if h, err = hashUpdateOrdered(hasher, h, hasher.Sum64()); err != nil { - return 0, err - } - } - return h, nil -} - -// hashUpdateOrdered is taken directly from -// https://github.com/mitchellh/hashstructure -func hashUpdateOrdered(h hash.Hash64, a, b uint64) (uint64, error) { - // For ordered updates, use a real hash function - h.Reset() - - e1 := binary.Write(h, binary.LittleEndian, a) - e2 := binary.Write(h, binary.LittleEndian, b) - if e1 != nil { - return 0, e1 - } - if e2 != nil { - return 0, e2 - } - return h.Sum64(), nil + return r.grants.GrantHash(ctx) } diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index 56817b541a..06c4efced9 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -206,7 +206,7 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error { services.RegisterScopeServiceServer(s, os) } if _, ok := currentServices[services.UserService_ServiceDesc.ServiceName]; !ok { - us, err := users.NewService(c.baseContext, c.IamRepoFn, c.conf.RawConfig.Controller.MaxPageSize) + us, err := users.NewService(c.baseContext, c.IamRepoFn, c.TargetAliasRepoFn, c.conf.RawConfig.Controller.MaxPageSize) if err != nil { return fmt.Errorf("failed to create user handler service: %w", err) } diff --git a/internal/daemon/controller/handlers/users/user_service.go b/internal/daemon/controller/handlers/users/user_service.go index 477c232540..7adc561511 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -8,11 +8,14 @@ import ( "fmt" "github.com/hashicorp/boundary/globals" + talias "github.com/hashicorp/boundary/internal/alias/target" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/common" "github.com/hashicorp/boundary/internal/daemon/controller/common/scopeids" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/event" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/iam/store" @@ -23,6 +26,7 @@ import ( "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" + aliaspb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/aliases" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/users" "github.com/hashicorp/go-secure-stdlib/strutil" @@ -43,6 +47,7 @@ var ( action.AddAccounts, action.SetAccounts, action.RemoveAccounts, + action.ListResolvableAliases, ) // CollectionActions contains the set of actions that can be performed on @@ -71,21 +76,25 @@ type Service struct { pbs.UnsafeUserServiceServer repoFn common.IamRepoFactory + aliasRepoFn common.TargetAliasRepoFactory maxPageSize uint } var _ pbs.UserServiceServer = (*Service)(nil) // NewService returns a user service which handles user related requests to boundary. -func NewService(ctx context.Context, repo common.IamRepoFactory, maxPageSize uint) (Service, error) { +func NewService(ctx context.Context, repo common.IamRepoFactory, aliasRepoFn common.TargetAliasRepoFactory, maxPageSize uint) (Service, error) { const op = "users.NewService" - if repo == nil { + switch { + case repo == nil: return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository") + case aliasRepoFn == nil: + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing alias repository") } if maxPageSize == 0 { maxPageSize = uint(globals.DefaultMaxPageSize) } - return Service{repoFn: repo, maxPageSize: maxPageSize}, nil + return Service{repoFn: repo, aliasRepoFn: aliasRepoFn, maxPageSize: maxPageSize}, nil } // ListUsers implements the interface pbs.UserServiceServer. @@ -468,6 +477,169 @@ func (s Service) RemoveUserAccounts(ctx context.Context, req *pbs.RemoveUserAcco return &pbs.RemoveUserAccountsResponse{Item: item}, nil } +// ListResolvableAliases implements the interface pbs.AliasServiceServer. +func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolvableAliasesRequest) (*pbs.ListResolvableAliasesResponse, error) { + const op = "users.(Service).ListResolvableAliases" + if err := validateListResolvableAliasesRequest(req); err != nil { + return nil, err + } + + authResults := s.authResult(ctx, req.GetId(), action.ListResolvableAliases) + if authResults.Error != nil { + return nil, authResults.Error + } + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(ctx, errors.Internal, op, "no request context found") + } + acl := authResults.ACL() + grantsHash, err := authResults.GrantsHash(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + if req.GetId() != authResults.UserId { + var err error + acl, grantsHash, err = s.aclAndGrantHashForUser(ctx, req.GetId()) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithoutEvent()) + } + } + + permissions := acl.ListResolvablePermissions(resource.Target, + action.Difference(targets.IdActions, action.NewActionSet(action.NoOp))) + + if len(permissions) == 0 { + // if there are no permitted targets then there will be no aliases that + // can resolve to them. + return &pbs.ListResolvableAliasesResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + }, nil + } + event.WriteSysEvent(ctx, op, "permissions found") + + 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()) + } + + filterItemFn := func(ctx context.Context, item *talias.Alias) (bool, error) { + return true, nil + } + + repo, err := s.aliasRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + var listResp *pagination.ListResponse[*talias.Alias] + var sortBy string + if req.GetListToken() == "" { + sortBy = "created_time" + listResp, err = talias.ListResolvableAliases(ctx, grantsHash, pageSize, filterItemFn, repo, permissions) + if err != nil { + return nil, err + } + } else { + listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.Alias, grantsHash) + if err != nil { + return nil, err + } + switch st := listToken.Subtype.(type) { + case *listtoken.PaginationToken: + sortBy = "created_time" + listResp, err = talias.ListResolvableAliasesPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, permissions) + if err != nil { + return nil, err + } + case *listtoken.StartRefreshToken: + sortBy = "updated_time" + listResp, err = talias.ListResolvableAliasesRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, permissions) + if err != nil { + return nil, err + } + case *listtoken.RefreshToken: + sortBy = "updated_time" + listResp, err = talias.ListResolvableAliasesRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, permissions) + if err != nil { + return nil, err + } + default: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st) + } + } + + finalItems := make([]*aliaspb.Alias, 0, len(listResp.Items)) + for _, item := range listResp.Items { + item, err := toResolvableAliasProto(item, handlers.WithOutputFields(outputFields)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + finalItems = append(finalItems, item) + } + respType := "delta" + if listResp.CompleteListing { + respType = "complete" + } + resp := &pbs.ListResolvableAliasesResponse{ + 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_ALIAS) + if err != nil { + return nil, err + } + } + return resp, nil +} + +// aclAndGrantHashForUser returns an ACL from the grants provided to the user and +// the hash of those grants. +func (s Service) aclAndGrantHashForUser(ctx context.Context, userId string) (perms.ACL, []byte, error) { + const op = "users.(Service).aclAndGrantHashForUser" + iamRepo, err := s.repoFn() + if err != nil { + return perms.ACL{}, nil, errors.Wrap(ctx, err, op, errors.WithoutEvent()) + } + grantTuples, err := iamRepo.GrantsForUser(ctx, userId) + if err != nil { + return perms.ACL{}, nil, errors.Wrap(ctx, err, op, errors.WithoutEvent()) + } + hash, err := grantTuples.GrantHash(ctx) + if err != nil { + return perms.ACL{}, nil, errors.Wrap(ctx, err, op, errors.WithoutEvent()) + } + parsedGrants := make([]perms.Grant, 0, len(grantTuples)) + // Note: Below, we always skip validation so that we don't error on formats + // that we've since restricted, e.g. "ids=foo;actions=create,read". These + // will simply not have an effect. + for _, pair := range grantTuples { + permsOpts := []perms.Option{ + perms.WithUserId(userId), + perms.WithSkipFinalValidation(true), + } + parsed, err := perms.Parse( + ctx, + pair.ScopeId, + pair.Grant, + permsOpts...) + if err != nil { + return perms.ACL{}, nil, errors.Wrap(ctx, err, op) + } + parsedGrants = append(parsedGrants, parsed) + } + return perms.NewACL(parsedGrants...), hash, nil +} + func (s Service) getFromRepo(ctx context.Context, id string) (*iam.User, []string, error) { repo, err := s.repoFn() if err != nil { @@ -723,6 +895,39 @@ func toProto(ctx context.Context, in *iam.User, accts []string, opt ...handlers. return &out, nil } +func toResolvableAliasProto(a *talias.Alias, opt ...handlers.Option) (*aliaspb.Alias, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building user proto") + } + outputFields := *opts.WithOutputFields + + pbItem := &aliaspb.Alias{} + if a == nil { + return pbItem, nil + } + if outputFields.Has(globals.IdField) { + pbItem.Id = a.GetPublicId() + } + if outputFields.Has(globals.CreatedTimeField) { + pbItem.CreatedTime = a.GetCreateTime().GetTimestamp() + } + if outputFields.Has(globals.UpdatedTimeField) { + pbItem.UpdatedTime = a.GetUpdateTime().GetTimestamp() + } + if outputFields.Has(globals.ValueField) { + pbItem.Value = a.GetValue() + } + if outputFields.Has(globals.DestinationIdField) && a.GetDestinationId() != "" { + pbItem.DestinationId = wrapperspb.String(a.GetDestinationId()) + } + if outputFields.Has(globals.TypeField) { + pbItem.Type = "target" + } + + return pbItem, nil +} + // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce // requirements on the structure of the request. They verify that: // - The path passed in is correctly formatted @@ -850,6 +1055,20 @@ func validateRemoveUserAccountsRequest(req *pbs.RemoveUserAccountsRequest) error return nil } +func validateListResolvableAliasesRequest(req *pbs.ListResolvableAliasesRequest) error { + badFields := map[string]string{} + if !handlers.ValidId(handlers.Id(req.GetId()), globals.UserPrefix) { + badFields[globals.IdField] = "Incorrectly formatted identifier." + } + if req.GetId() == globals.RecoveryUserId { + badFields["principal_ids"] = "Cannot list resolvable aliases for the recovery user." + } + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields) + } + return nil +} + func newOutputOpts(ctx context.Context, item *iam.User, scopeInfoMap map[string]*scopes.ScopeInfo, authResults auth.VerifyResults) ([]handlers.Option, bool) { res := perms.Resource{ Type: resource.User, diff --git a/internal/daemon/controller/handlers/users/user_service_test.go b/internal/daemon/controller/handlers/users/user_service_test.go index f1e6a41cf0..1a964f07cc 100644 --- a/internal/daemon/controller/handlers/users/user_service_test.go +++ b/internal/daemon/controller/handlers/users/user_service_test.go @@ -14,11 +14,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" + talias "github.com/hashicorp/boundary/internal/alias/target" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/common" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/users" "github.com/hashicorp/boundary/internal/db" @@ -28,7 +30,9 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/server" + "github.com/hashicorp/boundary/internal/target/tcp" "github.com/hashicorp/boundary/internal/types/scope" + pbalias "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/aliases" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/users" "google.golang.org/genproto/protobuf/field_mask" @@ -42,26 +46,30 @@ import ( "github.com/stretchr/testify/require" ) -var testAuthorizedActions = []string{"no-op", "read", "update", "delete", "add-accounts", "set-accounts", "remove-accounts"} +var testAuthorizedActions = []string{"no-op", "read", "update", "delete", "add-accounts", "set-accounts", "remove-accounts", "list-resolvable-aliases"} -func createDefaultUserAndRepo(t *testing.T, withAccts bool) (*iam.User, []string, func() (*iam.Repository, error)) { +func createDefaultUserAndRepos(t *testing.T, withAccts bool) (*iam.User, []string, common.IamRepoFactory, common.TargetAliasRepoFactory) { t.Helper() + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + rw := db.New(conn) repo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return repo, nil } + aliasRepoFn := func() (*talias.Repository, error) { + return talias.NewRepository(context.Background(), rw, rw, kmsCache) + } o, _ := iam.TestScopes(t, repo) u := iam.TestUser(t, repo, o.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) switch withAccts { case false: - return u, nil, repoFn + return u, nil, repoFn, aliasRepoFn default: require := require.New(t) - ctx := context.Background() - kmsCache := kms.TestKms(t, conn, wrap) databaseWrap, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) require.NoError(err) primaryAm := oidc.TestAuthMethod(t, conn, databaseWrap, o.PublicId, oidc.ActivePublicState, "alice-rp", "fido", @@ -84,12 +92,12 @@ func createDefaultUserAndRepo(t *testing.T, withAccts bool) (*iam.User, []string // reload the user with their accounts u, accts, err := repo.LookupUser(ctx, u.PublicId) require.NoError(err) - return u, accts, repoFn + return u, accts, repoFn, aliasRepoFn } } func TestGet(t *testing.T) { - u, uAccts, repoFn := createDefaultUserAndRepo(t, true) + u, uAccts, repoFn, aliasRepo := createDefaultUserAndRepos(t, true) toMerge := &pbs.GetUserRequest{ Id: u.GetPublicId(), @@ -149,7 +157,7 @@ func TestGet(t *testing.T) { req := proto.Clone(toMerge).(*pbs.GetUserRequest) proto.Merge(req, tc.req) - s, err := users.NewService(context.Background(), repoFn, 1000) + s, err := users.NewService(context.Background(), repoFn, aliasRepo, 1000) require.NoError(err, "Couldn't create new user service.") got, gErr := s.GetUser(auth.DisabledAuthTestContext(repoFn, u.GetScopeId()), req) @@ -172,18 +180,20 @@ func TestGet(t *testing.T) { func TestList(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } - repo, err := repoFn() - require.NoError(t, err) + aliasRepoFn := func() (*talias.Repository, error) { + return talias.NewRepository(context.Background(), rw, rw, kmsCache) + } - oNoUsers, _ := iam.TestScopes(t, repo) - oWithUsers, _ := iam.TestScopes(t, repo) + oNoUsers, _ := iam.TestScopes(t, iamRepo) + oWithUsers, _ := iam.TestScopes(t, iamRepo) - kmsCache := kms.TestKms(t, conn, wrap) databaseWrap, err := kmsCache.GetWrapper(context.Background(), oWithUsers.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) primaryAm := oidc.TestAuthMethod(t, conn, databaseWrap, oWithUsers.PublicId, oidc.ActivePublicState, "alice-rp", "fido", @@ -191,12 +201,12 @@ func TestList(t *testing.T) { oidc.WithApiUrl(oidc.TestConvertToUrls(t, "http://localhost:9200")[0]), oidc.WithSigningAlgs(oidc.RS256), ) - iam.TestSetPrimaryAuthMethod(t, repo, oWithUsers, primaryAm.PublicId) + iam.TestSetPrimaryAuthMethod(t, iamRepo, oWithUsers, primaryAm.PublicId) secondaryAm := password.TestAuthMethods(t, conn, oWithUsers.PublicId, 1) require.Len(t, secondaryAm, 1) - s, err := users.NewService(context.Background(), repoFn, 1000) + s, err := users.NewService(context.Background(), repoFn, aliasRepoFn, 1000) require.NoError(t, err) var wantUsers []*pb.User @@ -220,16 +230,16 @@ func TestList(t *testing.T) { for i := 0; i < 10; i++ { newU, err := iam.NewUser(ctx, oWithUsers.GetPublicId()) require.NoError(t, err) - u, err := repo.CreateUser(context.Background(), newU) + u, err := iamRepo.CreateUser(context.Background(), newU) require.NoError(t, err) oidcAcct := oidc.TestAccount(t, conn, primaryAm, fmt.Sprintf("alice+%d", i), oidc.WithFullName("Alice Eve Smith"), oidc.WithEmail("alice@smith.com")) pwAcct := password.TestAccount(t, conn, secondaryAm[0].PublicId, fmt.Sprintf("alice+%d", i)) - added, err := repo.AddUserAccounts(ctx, u.PublicId, u.Version, []string{oidcAcct.PublicId, pwAcct.PublicId}) + added, err := iamRepo.AddUserAccounts(ctx, u.PublicId, u.Version, []string{oidcAcct.PublicId, pwAcct.PublicId}) require.NoError(t, err) require.Len(t, added, 2) - u, _, err = repo.LookupUser(ctx, u.PublicId) + u, _, err = iamRepo.LookupUser(ctx, u.PublicId) require.NoError(t, err) wantUsers = append(wantUsers, &pb.User{ Id: u.GetPublicId(), @@ -375,7 +385,7 @@ func userToProto(u *iam.User, si *scopes.ScopeInfo, authorizedActions []string) CreatedTime: u.GetCreateTime().GetTimestamp(), UpdatedTime: u.GetUpdateTime().GetTimestamp(), Version: u.GetVersion(), - AuthorizedActions: testAuthorizedActions, + AuthorizedActions: authorizedActions, } if u.GetName() != "" { pu.Name = wrapperspb.String(u.GetName()) @@ -405,6 +415,9 @@ func TestListPagination(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } + aliasRepoFn := func() (*talias.Repository, error) { + return talias.NewRepository(ctx, rw, rw, kms) + } tokenRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } @@ -478,7 +491,7 @@ func TestListPagination(t *testing.T) { } slices.Reverse(allUsers) - a, err := users.NewService(ctx, iamRepoFn, 1000) + a, err := users.NewService(ctx, iamRepoFn, aliasRepoFn, 1000) require.NoError(t, err, "Couldn't create new user service.") // Run analyze to update postgres estimates @@ -751,10 +764,355 @@ func TestListPagination(t *testing.T) { assert.ErrorIs(t, handlers.ForbiddenError(), err) } +func TestListResolvableAliasesPagination(t *testing.T) { + // Set database read timeout to avoid duplicates in response + oldReadTimeout := globals.RefreshReadLookbackDuration + globals.RefreshReadLookbackDuration = 0 + t.Cleanup(func() { + globals.RefreshReadLookbackDuration = oldReadTimeout + }) + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + sqlDB, err := conn.SqlDB(ctx) + require.NoError(t, err) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + aliasRepoFn := func() (*talias.Repository, error) { + return talias.NewRepository(ctx, rw, rw, kms) + } + tokenRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(ctx, rw, rw, kms) + } + serversRepoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, kms) + } + tokenRepo, err := tokenRepoFn() + require.NoError(t, err) + + // Run analyze to update postgres meta tables + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + _, p := iam.TestScopes(t, iamRepo) + tar := tcp.TestTarget(ctx, t, conn, p.GetPublicId(), "resolvable") + + _, unresolvableP := iam.TestScopes(t, iamRepo) + unresolvedTar := tcp.TestTarget(ctx, t, conn, unresolvableP.GetPublicId(), "unresolvable") + // Create an alias that shouldn't be included in the paginated list results. + talias.TestAlias(t, rw, "unresolved.alias", talias.WithDestinationId(unresolvedTar.GetPublicId())) + + authMethod := password.TestAuthMethods(t, conn, "global", 1)[0] + acct := password.TestAccount(t, conn, authMethod.GetPublicId(), "test_user") + u := iam.TestUser(t, iamRepo, "global", iam.WithAccountIds(acct.PublicId)) + + // add roles for requester to be able to perform all actions on everyone + allowedRole := iam.TestRole(t, conn, "global", iam.WithGrantScopeIds([]string{globals.GrantScopeThis, globals.GrantScopeDescendants})) + iam.TestRoleGrant(t, conn, allowedRole.GetPublicId(), "ids=*;type=*;actions=*") + iam.TestUserRole(t, conn, allowedRole.GetPublicId(), u.GetPublicId()) + + at, err := tokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId()) + require.NoError(t, err) + + // add roles for user whose resolvable aliases are being listed they can + // only see the aliases which resolve to targets in project p. + resolvingUsersAt := authtoken.TestAuthToken(t, conn, kms, scope.Global.String()) + roleForResolving := iam.TestRole(t, conn, "global", iam.WithGrantScopeIds([]string{p.GetPublicId(), globals.GrantScopeThis})) + iam.TestRoleGrant(t, conn, roleForResolving.GetPublicId(), "ids=*;type=target;actions=authorize-session") + iam.TestRoleGrant(t, conn, roleForResolving.GetPublicId(), "ids={{.User.Id}};type=user;actions=list-resolvable-aliases") + iam.TestUserRole(t, conn, roleForResolving.GetPublicId(), resolvingUsersAt.GetIamUserId()) + + var allAliases []*talias.Alias + var allAliasPbs []*pbalias.Alias + var safeToRemoveAlias *talias.Alias + for i := 0; i < 10; i++ { + na := talias.TestAlias(t, rw, fmt.Sprintf("aliase%d.test", i), talias.WithDestinationId(tar.GetPublicId())) + allAliases = append(allAliases, na) + allAliasPbs = append(allAliasPbs, &pbalias.Alias{ + Id: na.GetPublicId(), + Value: na.GetValue(), + DestinationId: wrapperspb.String(na.GetDestinationId()), + CreatedTime: na.GetCreateTime().GetTimestamp(), + UpdatedTime: na.GetUpdateTime().GetTimestamp(), + Type: "target", + }) + + safeToRemoveAlias = na + } + slices.Reverse(allAliases) + slices.Reverse(allAliasPbs) + + a, err := users.NewService(ctx, iamRepoFn, aliasRepoFn, 1000) + require.NoError(t, err, "Couldn't create new user service.") + + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(context.Background(), "analyze") + require.NoError(t, err) + + // +1 because we have one alias that points to an unresolvable target + itemCount := uint32(len(allAliasPbs)) + 1 + testPageSize := int((itemCount - 2) / 2) + + // See that the resolvingUser can query themselves + requestInfo := authpb.RequestInfo{ + TokenFormat: uint32(auth.AuthTokenTypeBearer), + PublicId: resolvingUsersAt.GetPublicId(), + Token: resolvingUsersAt.GetToken(), + } + requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + got, err := a.ListResolvableAliases(ctx, &pbs.ListResolvableAliasesRequest{ + Id: resolvingUsersAt.GetIamUserId(), + }) + require.NoError(t, err) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListResolvableAliasesResponse{ + Items: allAliasPbs, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 10, + }, + protocmp.Transform(), + protocmp.SortRepeated(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }), + protocmp.IgnoreFields(&pbs.ListResolvableAliasesResponse{}, "list_token"), + ), + ) + + // This user cannot list resolvable for other aliases + _, err = a.ListResolvableAliases(ctx, &pbs.ListResolvableAliasesRequest{ + Id: at.GetIamUserId(), + }) + require.Error(t, err) + assert.ErrorIs(t, handlers.ForbiddenError(), err) + + // Now let the admin user list resolvable aliases + requestInfo = authpb.RequestInfo{ + TokenFormat: uint32(auth.AuthTokenTypeBearer), + PublicId: at.GetPublicId(), + Token: at.GetToken(), + } + requestContext = context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + + req := &pbs.ListResolvableAliasesRequest{ + Id: resolvingUsersAt.GetIamUserId(), + ListToken: "", + PageSize: uint32(testPageSize), + } + got, err = a.ListResolvableAliases(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), testPageSize) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListResolvableAliasesResponse{ + Items: allAliasPbs[0:testPageSize], + ResponseType: "delta", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + // In addition to the added users, there are the users added + // by the test setup when specifying the permissions of the + // requester + EstItemCount: itemCount, + }, + protocmp.SortRepeated(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListResolvableAliasesResponse{}, "list_token"), + ), + ) + + // Request second page + req.ListToken = got.ListToken + got, err = a.ListResolvableAliases(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), testPageSize) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListResolvableAliasesResponse{ + Items: allAliasPbs[testPageSize : testPageSize*2], + ResponseType: "delta", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: itemCount, + }, + protocmp.Transform(), + protocmp.SortRepeated(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }), + protocmp.IgnoreFields(&pbs.ListResolvableAliasesResponse{}, "list_token"), + ), + ) + + // Request rest of results + req.ListToken = got.ListToken + req.PageSize = 10 + got, err = a.ListResolvableAliases(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListResolvableAliasesResponse{ + Items: allAliasPbs[testPageSize*2:], + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: itemCount, + }, + protocmp.Transform(), + protocmp.SortRepeated(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }), + protocmp.IgnoreFields(&pbs.ListResolvableAliasesResponse{}, "list_token"), + ), + ) + + // Update 2 aliases and see them in the refresh + aliasRepo, err := aliasRepoFn() + require.NoError(t, err) + + for i := 0; i < 2; i++ { + r := allAliases[len(allAliasPbs)-1] + rPb := allAliasPbs[len(allAliasPbs)-1] + r.Description = fmt.Sprintf("updated%d", i) + + updated, _, err := aliasRepo.UpdateAlias(ctx, r, r.GetVersion(), []string{"description"}) + require.NoError(t, err) + + r.Version = updated.GetVersion() + r.UpdateTime = updated.GetUpdateTime() + rPb.UpdatedTime = updated.GetUpdateTime().GetTimestamp() + // ListResolvableAliases does not return the description, or the version + // so we do not update those values here in the protobuf aliases + allAliases = append([]*talias.Alias{r}, allAliases[:len(allAliases)-1]...) + allAliasPbs = append([]*pbalias.Alias{rPb}, allAliasPbs[:len(allAliasPbs)-1]...) + } + + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // Request updated results + req.ListToken = got.ListToken + req.PageSize = 1 + got, err = a.ListResolvableAliases(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListResolvableAliasesResponse{ + Items: []*pbalias.Alias{allAliasPbs[0]}, + ResponseType: "delta", + SortBy: "updated_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: itemCount, + }, + protocmp.Transform(), + protocmp.SortRepeated(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }), + protocmp.IgnoreFields(&pbs.ListResolvableAliasesResponse{}, "list_token"), + ), + ) + + // Get next page + req.ListToken = got.ListToken + got, err = a.ListResolvableAliases(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListResolvableAliasesResponse{ + Items: []*pbalias.Alias{allAliasPbs[1]}, + ResponseType: "complete", + SortBy: "updated_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: itemCount, + }, + protocmp.Transform(), + protocmp.SortRepeated(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }), + protocmp.IgnoreFields(&pbs.ListResolvableAliasesResponse{}, "list_token"), + ), + ) + + _, err = aliasRepo.DeleteAlias(ctx, safeToRemoveAlias.GetPublicId()) + require.NoError(t, err) + req.ListToken = got.ListToken + got, err = a.ListResolvableAliases(ctx, req) + require.NoError(t, err) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListResolvableAliasesResponse{ + Items: nil, + ResponseType: "complete", + SortBy: "updated_time", + SortDir: "desc", + RemovedIds: []string{safeToRemoveAlias.GetPublicId()}, + EstItemCount: itemCount, + }, + protocmp.Transform(), + protocmp.SortRepeated(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }), + protocmp.IgnoreFields(&pbs.ListResolvableAliasesResponse{}, "list_token"), + ), + ) + + // Create unauthenticated user + unauthAt := authtoken.TestAuthToken(t, conn, kms, scope.Global.String()) + unauthR := iam.TestRole(t, conn, p.GetPublicId()) + _ = iam.TestUserRole(t, conn, unauthR.GetPublicId(), unauthAt.GetIamUserId()) + + // Make a request with the unauthenticated user, + // ensure the response contains the pagination parameters. + requestInfo = authpb.RequestInfo{ + TokenFormat: uint32(auth.AuthTokenTypeBearer), + PublicId: unauthAt.GetPublicId(), + Token: unauthAt.GetToken(), + } + requestContext = context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + + _, err = a.ListResolvableAliases(ctx, &pbs.ListResolvableAliasesRequest{ + Id: resolvingUsersAt.GetIamUserId(), + }) + require.Error(t, err) + assert.ErrorIs(t, handlers.ForbiddenError(), err) +} + func TestDelete(t *testing.T) { - u, _, repoFn := createDefaultUserAndRepo(t, false) + u, _, repoFn, aliasRepoFn := createDefaultUserAndRepos(t, false) - s, err := users.NewService(context.Background(), repoFn, 1000) + s, err := users.NewService(context.Background(), repoFn, aliasRepoFn, 1000) require.NoError(t, err, "Error when getting new user service.") cases := []struct { @@ -799,9 +1157,9 @@ func TestDelete(t *testing.T) { func TestDelete_twice(t *testing.T) { assert, require := assert.New(t), require.New(t) - u, _, repoFn := createDefaultUserAndRepo(t, false) + u, _, repoFn, aliasRepoFn := createDefaultUserAndRepos(t, false) - s, err := users.NewService(context.Background(), repoFn, 1000) + s, err := users.NewService(context.Background(), repoFn, aliasRepoFn, 1000) require.NoError(err, "Error when getting new user service") req := &pbs.DeleteUserRequest{ Id: u.GetPublicId(), @@ -815,7 +1173,7 @@ func TestDelete_twice(t *testing.T) { } func TestCreate(t *testing.T) { - defaultUser, _, repoFn := createDefaultUserAndRepo(t, false) + defaultUser, _, repoFn, aliasRepoFn := createDefaultUserAndRepos(t, false) defaultCreated := defaultUser.GetCreateTime().GetTimestamp().AsTime() cases := []struct { @@ -893,7 +1251,7 @@ func TestCreate(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 := users.NewService(context.Background(), repoFn, 1000) + s, err := users.NewService(context.Background(), repoFn, aliasRepoFn, 1000) require.NoError(err, "Error when getting new user service.") got, gErr := s.CreateUser(auth.DisabledAuthTestContext(repoFn, tc.req.GetItem().GetScopeId()), tc.req) @@ -928,8 +1286,8 @@ func TestCreate(t *testing.T) { } func TestUpdate(t *testing.T) { - u, _, repoFn := createDefaultUserAndRepo(t, false) - tested, err := users.NewService(context.Background(), repoFn, 1000) + u, _, repoFn, aliasRepoFn := createDefaultUserAndRepos(t, false) + tested, err := users.NewService(context.Background(), repoFn, aliasRepoFn, 1000) require.NoError(t, err, "Error when getting new user service.") created := u.GetCreateTime().GetTimestamp().AsTime() @@ -1208,12 +1566,16 @@ func TestAddAccount(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) ctx := context.Background() + rw := db.New(conn) kmsCache := kms.TestKms(t, conn, wrap) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } - s, err := users.NewService(ctx, repoFn, 1000) + aliasRepoFn := func() (*talias.Repository, error) { + return talias.NewRepository(ctx, rw, rw, kmsCache) + } + s, err := users.NewService(ctx, repoFn, aliasRepoFn, 1000) require.NoError(t, err, "Error when getting new user service.") o, _ := iam.TestScopes(t, iamRepo) @@ -1368,12 +1730,16 @@ func TestSetAccount(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) ctx := context.Background() + rw := db.New(conn) kmsCache := kms.TestKms(t, conn, wrap) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } - s, err := users.NewService(ctx, repoFn, 1000) + aliasRepoFn := func() (*talias.Repository, error) { + return talias.NewRepository(ctx, rw, rw, kmsCache) + } + s, err := users.NewService(ctx, repoFn, aliasRepoFn, 1000) require.NoError(t, err, "Error when getting new user service.") o, _ := iam.TestScopes(t, iamRepo) @@ -1530,12 +1896,16 @@ func TestRemoveAccount(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) ctx := context.Background() + rw := db.New(conn) kmsCache := kms.TestKms(t, conn, wrap) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } - s, err := users.NewService(ctx, repoFn, 1000) + aliasRepoFn := func() (*talias.Repository, error) { + return talias.NewRepository(ctx, rw, rw, kmsCache) + } + s, err := users.NewService(ctx, repoFn, aliasRepoFn, 1000) require.NoError(t, err, "Error when getting new user service.") o, _ := iam.TestScopes(t, iamRepo) diff --git a/internal/daemon/controller/rate_limiter_test.go b/internal/daemon/controller/rate_limiter_test.go index ec5b6214f4..d899476f28 100644 --- a/internal/daemon/controller/rate_limiter_test.go +++ b/internal/daemon/controller/rate_limiter_test.go @@ -63,7 +63,7 @@ func Test_newRateLimiterConfig(t *testing.T) { ratelimit.DefaultLimiterMaxQuotas(), false, &rateLimiterConfig{ - maxSize: 336168, + maxSize: 338169, configs: nil, disabled: false, limits: defaultLimits, @@ -515,24 +515,24 @@ func Test_rateLimiterConfig_writeSysEvent(t *testing.T) { disabled bool }{ { - "defaults", - func(n string) error { + name: "defaults", + setup: func(n string) error { return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) }, - func() { event.TestResetSystEventer(t) }, - c.AllEvents.Name(), - nil, - ratelimit.DefaultLimiterMaxQuotas(), - false, + cleanup: func() { event.TestResetSystEventer(t) }, + sinkFileName: c.AllEvents.Name(), + configs: nil, + maxSize: ratelimit.DefaultLimiterMaxQuotas(), + disabled: false, }, { - "override", - func(n string) error { + name: "override", + setup: func(n string) error { return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) }, - func() { event.TestResetSystEventer(t) }, - c.AllEvents.Name(), - ratelimit.Configs{ + cleanup: func() { event.TestResetSystEventer(t) }, + sinkFileName: c.AllEvents.Name(), + configs: ratelimit.Configs{ { Resources: []string{"*"}, Actions: []string{"*"}, @@ -558,30 +558,30 @@ func Test_rateLimiterConfig_writeSysEvent(t *testing.T) { Unlimited: false, }, }, - ratelimit.DefaultLimiterMaxQuotas(), - false, + maxSize: ratelimit.DefaultLimiterMaxQuotas(), + disabled: false, }, { - "max_size", - func(n string) error { + name: "max_size", + setup: func(n string) error { return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) }, - func() { event.TestResetSystEventer(t) }, - c.AllEvents.Name(), - nil, - 3000, - false, + cleanup: func() { event.TestResetSystEventer(t) }, + sinkFileName: c.AllEvents.Name(), + configs: nil, + maxSize: 3000, + disabled: false, }, { - "disabled", - func(n string) error { + name: "disabled", + setup: func(n string) error { return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) }, - func() { event.TestResetSystEventer(t) }, - c.AllEvents.Name(), - nil, - 0, - true, + cleanup: func() { event.TestResetSystEventer(t) }, + sinkFileName: c.AllEvents.Name(), + configs: nil, + maxSize: 0, + disabled: true, }, } diff --git a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/defaults.json b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/defaults.json index 756c49a1af..dac2c5e1e1 100644 --- a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/defaults.json +++ b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/defaults.json @@ -3787,6 +3787,32 @@ "unlimited": false } ], + "list-resolvable-aliases": [ + { + "action": "list-resolvable-aliases", + "limit": 30000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "list-resolvable-aliases", + "limit": 30000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "list-resolvable-aliases", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], "no-op": [ { "action": "no-op", @@ -4233,7 +4259,7 @@ ] } }, - "max_size": 336168, + "max_size": 338169, "msg": "controller api rate limiter" }, "op": "controller.(rateLimiterConfig).writeSysEvent", diff --git a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/max_size.json b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/max_size.json index 32686920de..e61db2e471 100644 --- a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/max_size.json +++ b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/max_size.json @@ -3629,6 +3629,32 @@ "unlimited": false } ], + "list-resolvable-aliases": [ + { + "action": "list-resolvable-aliases", + "limit": 30000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "list-resolvable-aliases", + "limit": 30000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "list-resolvable-aliases", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], "no-op": [ { "action": "no-op", diff --git a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/override.json b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/override.json index 033b37f9d8..5a2b452c81 100644 --- a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/override.json +++ b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/override.json @@ -3629,6 +3629,32 @@ "unlimited": false } ], + "list-resolvable-aliases": [ + { + "action": "list-resolvable-aliases", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "list-resolvable-aliases", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "list-resolvable-aliases", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], "no-op": [ { "action": "no-op", @@ -4075,7 +4101,7 @@ ] } }, - "max_size": 336168, + "max_size": 338169, "msg": "controller api rate limiter" }, "op": "controller.(rateLimiterConfig).writeSysEvent", diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index efb731c8f4..ea4a405e2e 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -5223,6 +5223,53 @@ ] } }, + "/v1/users/{id}:list-resolvable-aliases": { + "get": { + "summary": "Lists all Aliases which point to a resource for which the requester has some permission.", + "operationId": "UserService_ListResolvableAliases", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/controller.api.services.v1.ListResolvableAliasesResponse" + } + }, + "default": { + "description": "Returned when there is an error processing the request.", + "schema": { + "$ref": "#/definitions/controller.api.v1.Error" + } + } + }, + "parameters": [ + { + "name": "id", + "description": "", + "in": "path", + "required": true, + "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 default page configured,\nan error will be returned.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "User service" + ] + } + }, "/v1/users/{id}:remove-accounts": { "post": { "summary": "Removes the specified Accounts from being associated with the provided User.", @@ -9896,6 +9943,46 @@ } } }, + "controller.api.services.v1.ListResolvableAliasesResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/controller.api.resources.aliases.v1.Alias" + } + }, + "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. This includes aliases which have their\ndestination_id removed or set to a resource for which the requester doesn't\nhave permissions." + }, + "est_item_count": { + "type": "integer", + "format": "int64", + "description": "An estimate at the total items available. This may change during pagination." + } + } + }, "controller.api.services.v1.ListRolesResponse": { "type": "object", "properties": { diff --git a/internal/gen/controller/api/services/user_service.pb.go b/internal/gen/controller/api/services/user_service.pb.go index 7177d6e63f..664ca9aae6 100644 --- a/internal/gen/controller/api/services/user_service.pb.go +++ b/internal/gen/controller/api/services/user_service.pb.go @@ -11,6 +11,7 @@ package services import ( _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + aliases "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/aliases" users "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/users" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -947,6 +948,185 @@ func (x *RemoveUserAccountsResponse) GetItem() *users.User { return nil } +type ListResolvableAliasesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public" eventstream:"observation"` // @gotags: `class:"public" eventstream:"observation"` + // 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,2,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 default page configured, + // an error will be returned. + PageSize uint32 `protobuf:"varint,3,opt,name=page_size,proto3" json:"page_size,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *ListResolvableAliasesRequest) Reset() { + *x = ListResolvableAliasesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_user_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListResolvableAliasesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListResolvableAliasesRequest) ProtoMessage() {} + +func (x *ListResolvableAliasesRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_user_service_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListResolvableAliasesRequest.ProtoReflect.Descriptor instead. +func (*ListResolvableAliasesRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_user_service_proto_rawDescGZIP(), []int{16} +} + +func (x *ListResolvableAliasesRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ListResolvableAliasesRequest) GetListToken() string { + if x != nil { + return x.ListToken + } + return "" +} + +func (x *ListResolvableAliasesRequest) GetPageSize() uint32 { + if x != nil { + return x.PageSize + } + return 0 +} + +type ListResolvableAliasesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*aliases.Alias `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. This includes aliases which have their + // destination_id removed or set to a resource for which the requester doesn't + // have permissions. + 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 *ListResolvableAliasesResponse) Reset() { + *x = ListResolvableAliasesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_user_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListResolvableAliasesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListResolvableAliasesResponse) ProtoMessage() {} + +func (x *ListResolvableAliasesResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_user_service_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListResolvableAliasesResponse.ProtoReflect.Descriptor instead. +func (*ListResolvableAliasesResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_user_service_proto_rawDescGZIP(), []int{17} +} + +func (x *ListResolvableAliasesResponse) GetItems() []*aliases.Alias { + if x != nil { + return x.Items + } + return nil +} + +func (x *ListResolvableAliasesResponse) GetResponseType() string { + if x != nil { + return x.ResponseType + } + return "" +} + +func (x *ListResolvableAliasesResponse) GetListToken() string { + if x != nil { + return x.ListToken + } + return "" +} + +func (x *ListResolvableAliasesResponse) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +func (x *ListResolvableAliasesResponse) GetSortDir() string { + if x != nil { + return x.SortDir + } + return "" +} + +func (x *ListResolvableAliasesResponse) GetRemovedIds() []string { + if x != nil { + return x.RemovedIds + } + return nil +} + +func (x *ListResolvableAliasesResponse) GetEstItemCount() uint32 { + if x != nil { + return x.EstItemCount + } + return 0 +} + var File_controller_api_services_v1_user_service_proto protoreflect.FileDescriptor var file_controller_api_services_v1_user_service_proto_rawDesc = []byte{ @@ -954,242 +1134,289 @@ var file_controller_api_services_v1_user_service_proto_rawDesc = []byte{ 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1a, 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, 0x1a, 0x2c, 0x63, 0x6f, 0x6e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x75, - 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, - 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4e, 0x0a, 0x0f, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, - 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 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, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xa1, 0x01, 0x0a, 0x10, - 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 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, - 0x98, 0x02, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 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, - 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 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, 0x50, 0x0a, 0x11, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x2f, 0x76, 0x31, + 0x2f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2c, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x2f, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, + 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, + 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4e, 0x0a, 0x0f, + 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 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, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x63, 0x0a, 0x12, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 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, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x27, 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, 0x75, 0x73, - 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x22, 0x9e, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xa1, 0x01, 0x0a, + 0x10, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 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, 0x98, 0x02, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 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, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 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, 0x51, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 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, 0x50, 0x0a, 0x11, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 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, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x63, 0x0a, + 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 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, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x27, 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, 0x75, + 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x22, 0x9e, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 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, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x64, 0x0a, 0x16, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x73, 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, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, - 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x22, 0x56, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, - 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x27, 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, 0x75, 0x73, 0x65, 0x72, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x64, - 0x0a, 0x16, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 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, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x5f, 0x69, 0x64, 0x73, 0x22, 0x56, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 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, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x67, 0x0a, 0x19, - 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 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, 0x51, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 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, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x64, 0x0a, 0x16, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 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, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x22, 0x56, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x55, 0x73, + 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x27, 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, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, + 0x64, 0x0a, 0x16, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 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, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x5f, 0x69, 0x64, 0x73, 0x22, 0x59, 0x0a, 0x1a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x27, 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, 0x75, 0x73, 0x65, - 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, - 0x32, 0xe1, 0x0e, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x98, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2a, 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, 0x55, 0x73, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 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, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x34, 0x92, 0x41, 0x15, 0x12, 0x13, 0x47, 0x65, 0x74, 0x73, - 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x2f, 0x76, 0x31, - 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x90, 0x01, 0x0a, 0x09, - 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x2c, 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, 0x55, 0x73, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 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, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x92, 0x41, 0x12, 0x12, 0x10, 0x4c, 0x69, 0x73, - 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x55, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x0b, 0x12, 0x09, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0xa5, - 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2d, 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, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 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, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x92, 0x41, - 0x18, 0x12, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, - 0x67, 0x6c, 0x65, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x09, 0x2f, 0x76, 0x31, - 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0xa3, 0x01, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x74, 0x5f, 0x69, 0x64, 0x73, 0x22, 0x56, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 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, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x67, 0x0a, + 0x19, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 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, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x22, 0x59, 0x0a, 0x1a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x27, 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, 0x75, 0x73, + 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x22, 0x6c, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x61, + 0x62, 0x6c, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x02, 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, 0x03, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, + 0xa7, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x61, 0x62, + 0x6c, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x40, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2a, 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, 0x6c, 0x69, 0x61, + 0x73, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, 0x69, 0x61, 0x73, 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, 0x32, 0xfe, 0x10, 0x0a, 0x0b, 0x55, 0x73, + 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x98, 0x01, 0x0a, 0x07, 0x47, 0x65, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2a, 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, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 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, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x92, 0x41, 0x11, 0x12, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x73, 0x20, 0x61, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, - 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x0e, 0x2f, 0x76, - 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x97, 0x01, 0x0a, - 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2d, 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, 0x55, - 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 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, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x92, 0x41, 0x11, 0x12, - 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2e, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x2a, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, - 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xcd, 0x01, 0x0a, 0x0f, 0x41, 0x64, 0x64, 0x55, 0x73, - 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 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, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2b, 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, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x34, + 0x92, 0x41, 0x15, 0x12, 0x13, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, + 0x6c, 0x65, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x62, 0x04, + 0x69, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, + 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x90, 0x01, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x73, 0x12, 0x2c, 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, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2d, 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, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x26, 0x92, 0x41, 0x12, 0x12, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0b, 0x12, 0x09, 0x2f, 0x76, + 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0xa5, 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2d, 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, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 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, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x92, 0x41, 0x18, 0x12, 0x16, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x55, 0x73, 0x65, + 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, + 0x69, 0x74, 0x65, 0x6d, 0x22, 0x09, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, + 0xa3, 0x01, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2d, 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, 0x64, 0x64, 0x55, - 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, 0x22, 0x12, 0x20, 0x41, 0x73, 0x73, 0x6f, 0x63, 0x69, - 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x20, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, - 0x74, 0x6f, 0x20, 0x61, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, - 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x1b, 0x2f, 0x76, 0x31, 0x2f, 0x75, - 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x64, 0x64, 0x2d, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0xb5, 0x02, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x55, 0x73, - 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 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, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 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, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x92, + 0x41, 0x11, 0x12, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x55, 0x73, + 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, + 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, + 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x97, 0x01, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x12, 0x2d, 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, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 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, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x92, 0x41, 0x11, 0x12, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x73, 0x20, 0x61, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x2a, + 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, + 0xcd, 0x01, 0x0a, 0x0f, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 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, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 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, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, + 0x22, 0x12, 0x20, 0x41, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, + 0x20, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x55, 0x73, + 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x22, 0x1b, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x3a, 0x61, 0x64, 0x64, 0x2d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, + 0xb5, 0x02, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 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, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 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, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb8, 0x01, 0x92, + 0x41, 0x88, 0x01, 0x12, 0x85, 0x01, 0x53, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x20, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, + 0x64, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x55, 0x73, 0x65, 0x72, 0x20, 0x74, 0x6f, + 0x20, 0x65, 0x78, 0x61, 0x63, 0x74, 0x6c, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6c, 0x69, 0x73, + 0x74, 0x20, 0x6f, 0x66, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x20, 0x69, 0x6e, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2c, 0x20, 0x72, 0x65, + 0x6d, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x20, 0x61, 0x6e, 0x79, 0x20, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x61, 0x72, 0x65, 0x20, 0x6e, 0x6f, 0x74, + 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x26, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x1b, 0x2f, 0x76, 0x31, 0x2f, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x73, 0x65, 0x74, 0x2d, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x86, 0x02, 0x0a, 0x12, 0x52, 0x65, 0x6d, 0x6f, + 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 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, 0x53, 0x65, 0x74, 0x55, - 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0xb8, 0x01, 0x92, 0x41, 0x88, 0x01, 0x12, 0x85, 0x01, 0x53, 0x65, 0x74, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x20, 0x61, 0x73, - 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x55, 0x73, 0x65, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x78, 0x61, 0x63, 0x74, 0x6c, 0x79, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x2c, 0x20, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x20, 0x61, 0x6e, - 0x79, 0x20, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, - 0x61, 0x72, 0x65, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, - 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x22, 0x1b, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, - 0x7d, 0x3a, 0x73, 0x65, 0x74, 0x2d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x86, - 0x02, 0x0a, 0x12, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, + 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 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, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 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, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, - 0x55, 0x73, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x80, 0x01, 0x92, 0x41, 0x4e, 0x12, 0x4c, 0x52, 0x65, 0x6d, 0x6f, - 0x76, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, - 0x64, 0x20, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, - 0x62, 0x65, 0x69, 0x6e, 0x67, 0x20, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, - 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x64, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x29, 0x3a, 0x01, - 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x1e, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, - 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x2d, 0x61, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x1a, 0xaa, 0x02, 0x92, 0x41, 0xa6, 0x02, 0x0a, 0x0c, - 0x55, 0x73, 0x65, 0x72, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x99, 0x01, 0x41, - 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x68, - 0x75, 0x6d, 0x61, 0x6e, 0x20, 0x69, 0x6e, 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6c, 0x20, - 0x6f, 0x72, 0x20, 0x61, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x65, 0x73, 0x20, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x20, 0x54, 0x68, - 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x73, 0x20, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x42, - 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2e, 0x1a, 0x7a, 0x0a, 0x2d, 0x52, 0x65, 0x61, 0x64, - 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x75, 0x73, 0x65, 0x72, 0x73, 0x20, 0x69, 0x6e, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x20, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x49, 0x68, 0x74, 0x74, 0x70, 0x73, - 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x2e, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x61, 0x72, 0x79, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x63, 0x65, 0x70, 0x74, - 0x73, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2d, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2f, 0x75, - 0x73, 0x65, 0x72, 0x73, 0x42, 0x4d, 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, + 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x80, 0x01, + 0x92, 0x41, 0x4e, 0x12, 0x4c, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x62, 0x65, 0x69, 0x6e, 0x67, 0x20, 0x61, + 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x20, 0x55, 0x73, 0x65, 0x72, + 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x29, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x22, 0x1e, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, + 0x3a, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x2d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x12, 0x9a, 0x02, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x61, + 0x62, 0x6c, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x38, 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, 0x52, 0x65, 0x73, 0x6f, + 0x6c, 0x76, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 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, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x61, 0x62, 0x6c, 0x65, + 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x8b, 0x01, 0x92, 0x41, 0x5a, 0x12, 0x58, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, + 0x20, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x72, 0x20, 0x68, 0x61, 0x73, 0x20, 0x73, + 0x6f, 0x6d, 0x65, 0x20, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x12, 0x26, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, + 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x72, 0x65, 0x73, 0x6f, 0x6c, + 0x76, 0x61, 0x62, 0x6c, 0x65, 0x2d, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x1a, 0xaa, 0x02, + 0x92, 0x41, 0xa6, 0x02, 0x0a, 0x0c, 0x55, 0x73, 0x65, 0x72, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x99, 0x01, 0x41, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x63, 0x61, 0x6e, 0x20, + 0x62, 0x65, 0x20, 0x61, 0x20, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x20, 0x69, 0x6e, 0x64, 0x69, 0x76, + 0x69, 0x64, 0x75, 0x61, 0x6c, 0x20, 0x6f, 0x72, 0x20, 0x61, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, + 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, 0x73, 0x20, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x73, 0x20, 0x65, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6c, 0x65, 0x74, + 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x20, 0x69, 0x6e, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2e, 0x1a, 0x7a, + 0x0a, 0x2d, 0x52, 0x65, 0x61, 0x64, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x75, 0x73, 0x65, + 0x72, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, + 0x72, 0x79, 0x20, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, + 0x49, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, + 0x65, 0x72, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2f, 0x63, + 0x6f, 0x6e, 0x63, 0x65, 0x70, 0x74, 0x73, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2d, 0x6d, + 0x6f, 0x64, 0x65, 0x6c, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x42, 0x4d, 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 ( @@ -1204,59 +1431,65 @@ func file_controller_api_services_v1_user_service_proto_rawDescGZIP() []byte { return file_controller_api_services_v1_user_service_proto_rawDescData } -var file_controller_api_services_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_controller_api_services_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_controller_api_services_v1_user_service_proto_goTypes = []interface{}{ - (*GetUserRequest)(nil), // 0: controller.api.services.v1.GetUserRequest - (*GetUserResponse)(nil), // 1: controller.api.services.v1.GetUserResponse - (*ListUsersRequest)(nil), // 2: controller.api.services.v1.ListUsersRequest - (*ListUsersResponse)(nil), // 3: controller.api.services.v1.ListUsersResponse - (*CreateUserRequest)(nil), // 4: controller.api.services.v1.CreateUserRequest - (*CreateUserResponse)(nil), // 5: controller.api.services.v1.CreateUserResponse - (*UpdateUserRequest)(nil), // 6: controller.api.services.v1.UpdateUserRequest - (*UpdateUserResponse)(nil), // 7: controller.api.services.v1.UpdateUserResponse - (*DeleteUserRequest)(nil), // 8: controller.api.services.v1.DeleteUserRequest - (*DeleteUserResponse)(nil), // 9: controller.api.services.v1.DeleteUserResponse - (*AddUserAccountsRequest)(nil), // 10: controller.api.services.v1.AddUserAccountsRequest - (*AddUserAccountsResponse)(nil), // 11: controller.api.services.v1.AddUserAccountsResponse - (*SetUserAccountsRequest)(nil), // 12: controller.api.services.v1.SetUserAccountsRequest - (*SetUserAccountsResponse)(nil), // 13: controller.api.services.v1.SetUserAccountsResponse - (*RemoveUserAccountsRequest)(nil), // 14: controller.api.services.v1.RemoveUserAccountsRequest - (*RemoveUserAccountsResponse)(nil), // 15: controller.api.services.v1.RemoveUserAccountsResponse - (*users.User)(nil), // 16: controller.api.resources.users.v1.User - (*fieldmaskpb.FieldMask)(nil), // 17: google.protobuf.FieldMask + (*GetUserRequest)(nil), // 0: controller.api.services.v1.GetUserRequest + (*GetUserResponse)(nil), // 1: controller.api.services.v1.GetUserResponse + (*ListUsersRequest)(nil), // 2: controller.api.services.v1.ListUsersRequest + (*ListUsersResponse)(nil), // 3: controller.api.services.v1.ListUsersResponse + (*CreateUserRequest)(nil), // 4: controller.api.services.v1.CreateUserRequest + (*CreateUserResponse)(nil), // 5: controller.api.services.v1.CreateUserResponse + (*UpdateUserRequest)(nil), // 6: controller.api.services.v1.UpdateUserRequest + (*UpdateUserResponse)(nil), // 7: controller.api.services.v1.UpdateUserResponse + (*DeleteUserRequest)(nil), // 8: controller.api.services.v1.DeleteUserRequest + (*DeleteUserResponse)(nil), // 9: controller.api.services.v1.DeleteUserResponse + (*AddUserAccountsRequest)(nil), // 10: controller.api.services.v1.AddUserAccountsRequest + (*AddUserAccountsResponse)(nil), // 11: controller.api.services.v1.AddUserAccountsResponse + (*SetUserAccountsRequest)(nil), // 12: controller.api.services.v1.SetUserAccountsRequest + (*SetUserAccountsResponse)(nil), // 13: controller.api.services.v1.SetUserAccountsResponse + (*RemoveUserAccountsRequest)(nil), // 14: controller.api.services.v1.RemoveUserAccountsRequest + (*RemoveUserAccountsResponse)(nil), // 15: controller.api.services.v1.RemoveUserAccountsResponse + (*ListResolvableAliasesRequest)(nil), // 16: controller.api.services.v1.ListResolvableAliasesRequest + (*ListResolvableAliasesResponse)(nil), // 17: controller.api.services.v1.ListResolvableAliasesResponse + (*users.User)(nil), // 18: controller.api.resources.users.v1.User + (*fieldmaskpb.FieldMask)(nil), // 19: google.protobuf.FieldMask + (*aliases.Alias)(nil), // 20: controller.api.resources.aliases.v1.Alias } var file_controller_api_services_v1_user_service_proto_depIdxs = []int32{ - 16, // 0: controller.api.services.v1.GetUserResponse.item:type_name -> controller.api.resources.users.v1.User - 16, // 1: controller.api.services.v1.ListUsersResponse.items:type_name -> controller.api.resources.users.v1.User - 16, // 2: controller.api.services.v1.CreateUserRequest.item:type_name -> controller.api.resources.users.v1.User - 16, // 3: controller.api.services.v1.CreateUserResponse.item:type_name -> controller.api.resources.users.v1.User - 16, // 4: controller.api.services.v1.UpdateUserRequest.item:type_name -> controller.api.resources.users.v1.User - 17, // 5: controller.api.services.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask - 16, // 6: controller.api.services.v1.UpdateUserResponse.item:type_name -> controller.api.resources.users.v1.User - 16, // 7: controller.api.services.v1.AddUserAccountsResponse.item:type_name -> controller.api.resources.users.v1.User - 16, // 8: controller.api.services.v1.SetUserAccountsResponse.item:type_name -> controller.api.resources.users.v1.User - 16, // 9: controller.api.services.v1.RemoveUserAccountsResponse.item:type_name -> controller.api.resources.users.v1.User - 0, // 10: controller.api.services.v1.UserService.GetUser:input_type -> controller.api.services.v1.GetUserRequest - 2, // 11: controller.api.services.v1.UserService.ListUsers:input_type -> controller.api.services.v1.ListUsersRequest - 4, // 12: controller.api.services.v1.UserService.CreateUser:input_type -> controller.api.services.v1.CreateUserRequest - 6, // 13: controller.api.services.v1.UserService.UpdateUser:input_type -> controller.api.services.v1.UpdateUserRequest - 8, // 14: controller.api.services.v1.UserService.DeleteUser:input_type -> controller.api.services.v1.DeleteUserRequest - 10, // 15: controller.api.services.v1.UserService.AddUserAccounts:input_type -> controller.api.services.v1.AddUserAccountsRequest - 12, // 16: controller.api.services.v1.UserService.SetUserAccounts:input_type -> controller.api.services.v1.SetUserAccountsRequest - 14, // 17: controller.api.services.v1.UserService.RemoveUserAccounts:input_type -> controller.api.services.v1.RemoveUserAccountsRequest - 1, // 18: controller.api.services.v1.UserService.GetUser:output_type -> controller.api.services.v1.GetUserResponse - 3, // 19: controller.api.services.v1.UserService.ListUsers:output_type -> controller.api.services.v1.ListUsersResponse - 5, // 20: controller.api.services.v1.UserService.CreateUser:output_type -> controller.api.services.v1.CreateUserResponse - 7, // 21: controller.api.services.v1.UserService.UpdateUser:output_type -> controller.api.services.v1.UpdateUserResponse - 9, // 22: controller.api.services.v1.UserService.DeleteUser:output_type -> controller.api.services.v1.DeleteUserResponse - 11, // 23: controller.api.services.v1.UserService.AddUserAccounts:output_type -> controller.api.services.v1.AddUserAccountsResponse - 13, // 24: controller.api.services.v1.UserService.SetUserAccounts:output_type -> controller.api.services.v1.SetUserAccountsResponse - 15, // 25: controller.api.services.v1.UserService.RemoveUserAccounts:output_type -> controller.api.services.v1.RemoveUserAccountsResponse - 18, // [18:26] is the sub-list for method output_type - 10, // [10:18] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 18, // 0: controller.api.services.v1.GetUserResponse.item:type_name -> controller.api.resources.users.v1.User + 18, // 1: controller.api.services.v1.ListUsersResponse.items:type_name -> controller.api.resources.users.v1.User + 18, // 2: controller.api.services.v1.CreateUserRequest.item:type_name -> controller.api.resources.users.v1.User + 18, // 3: controller.api.services.v1.CreateUserResponse.item:type_name -> controller.api.resources.users.v1.User + 18, // 4: controller.api.services.v1.UpdateUserRequest.item:type_name -> controller.api.resources.users.v1.User + 19, // 5: controller.api.services.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask + 18, // 6: controller.api.services.v1.UpdateUserResponse.item:type_name -> controller.api.resources.users.v1.User + 18, // 7: controller.api.services.v1.AddUserAccountsResponse.item:type_name -> controller.api.resources.users.v1.User + 18, // 8: controller.api.services.v1.SetUserAccountsResponse.item:type_name -> controller.api.resources.users.v1.User + 18, // 9: controller.api.services.v1.RemoveUserAccountsResponse.item:type_name -> controller.api.resources.users.v1.User + 20, // 10: controller.api.services.v1.ListResolvableAliasesResponse.items:type_name -> controller.api.resources.aliases.v1.Alias + 0, // 11: controller.api.services.v1.UserService.GetUser:input_type -> controller.api.services.v1.GetUserRequest + 2, // 12: controller.api.services.v1.UserService.ListUsers:input_type -> controller.api.services.v1.ListUsersRequest + 4, // 13: controller.api.services.v1.UserService.CreateUser:input_type -> controller.api.services.v1.CreateUserRequest + 6, // 14: controller.api.services.v1.UserService.UpdateUser:input_type -> controller.api.services.v1.UpdateUserRequest + 8, // 15: controller.api.services.v1.UserService.DeleteUser:input_type -> controller.api.services.v1.DeleteUserRequest + 10, // 16: controller.api.services.v1.UserService.AddUserAccounts:input_type -> controller.api.services.v1.AddUserAccountsRequest + 12, // 17: controller.api.services.v1.UserService.SetUserAccounts:input_type -> controller.api.services.v1.SetUserAccountsRequest + 14, // 18: controller.api.services.v1.UserService.RemoveUserAccounts:input_type -> controller.api.services.v1.RemoveUserAccountsRequest + 16, // 19: controller.api.services.v1.UserService.ListResolvableAliases:input_type -> controller.api.services.v1.ListResolvableAliasesRequest + 1, // 20: controller.api.services.v1.UserService.GetUser:output_type -> controller.api.services.v1.GetUserResponse + 3, // 21: controller.api.services.v1.UserService.ListUsers:output_type -> controller.api.services.v1.ListUsersResponse + 5, // 22: controller.api.services.v1.UserService.CreateUser:output_type -> controller.api.services.v1.CreateUserResponse + 7, // 23: controller.api.services.v1.UserService.UpdateUser:output_type -> controller.api.services.v1.UpdateUserResponse + 9, // 24: controller.api.services.v1.UserService.DeleteUser:output_type -> controller.api.services.v1.DeleteUserResponse + 11, // 25: controller.api.services.v1.UserService.AddUserAccounts:output_type -> controller.api.services.v1.AddUserAccountsResponse + 13, // 26: controller.api.services.v1.UserService.SetUserAccounts:output_type -> controller.api.services.v1.SetUserAccountsResponse + 15, // 27: controller.api.services.v1.UserService.RemoveUserAccounts:output_type -> controller.api.services.v1.RemoveUserAccountsResponse + 17, // 28: controller.api.services.v1.UserService.ListResolvableAliases:output_type -> controller.api.services.v1.ListResolvableAliasesResponse + 20, // [20:29] is the sub-list for method output_type + 11, // [11:20] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_controller_api_services_v1_user_service_proto_init() } @@ -1457,6 +1690,30 @@ func file_controller_api_services_v1_user_service_proto_init() { return nil } } + file_controller_api_services_v1_user_service_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListResolvableAliasesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_user_service_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListResolvableAliasesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1464,7 +1721,7 @@ func file_controller_api_services_v1_user_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_services_v1_user_service_proto_rawDesc, NumEnums: 0, - NumMessages: 16, + NumMessages: 18, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/gen/controller/api/services/user_service.pb.gw.go b/internal/gen/controller/api/services/user_service.pb.gw.go index 13eb1fe098..68033362f3 100644 --- a/internal/gen/controller/api/services/user_service.pb.gw.go +++ b/internal/gen/controller/api/services/user_service.pb.gw.go @@ -477,6 +477,76 @@ func local_request_UserService_RemoveUserAccounts_0(ctx context.Context, marshal } +var ( + filter_UserService_ListResolvableAliases_0 = &utilities.DoubleArray{Encoding: map[string]int{"id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} +) + +func request_UserService_ListResolvableAliases_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListResolvableAliasesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListResolvableAliases_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ListResolvableAliases(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_UserService_ListResolvableAliases_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListResolvableAliasesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListResolvableAliases_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.ListResolvableAliases(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterUserServiceHandlerServer registers the http handlers for service UserService to "mux". // UnaryRPC :call UserServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -683,6 +753,31 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux }) + mux.Handle("GET", pattern_UserService_ListResolvableAliases_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.UserService/ListResolvableAliases", runtime.WithHTTPPathPattern("/v1/users/{id}:list-resolvable-aliases")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_ListResolvableAliases_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_UserService_ListResolvableAliases_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -900,6 +995,28 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux }) + mux.Handle("GET", pattern_UserService_ListResolvableAliases_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.UserService/ListResolvableAliases", runtime.WithHTTPPathPattern("/v1/users/{id}:list-resolvable-aliases")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_ListResolvableAliases_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_UserService_ListResolvableAliases_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -973,6 +1090,8 @@ var ( pattern_UserService_SetUserAccounts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "users", "id"}, "set-accounts")) pattern_UserService_RemoveUserAccounts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "users", "id"}, "remove-accounts")) + + pattern_UserService_ListResolvableAliases_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "users", "id"}, "list-resolvable-aliases")) ) var ( @@ -991,4 +1110,6 @@ var ( forward_UserService_SetUserAccounts_0 = runtime.ForwardResponseMessage forward_UserService_RemoveUserAccounts_0 = runtime.ForwardResponseMessage + + forward_UserService_ListResolvableAliases_0 = runtime.ForwardResponseMessage ) diff --git a/internal/gen/controller/api/services/user_service_grpc.pb.go b/internal/gen/controller/api/services/user_service_grpc.pb.go index 5859f37b85..ca2538c719 100644 --- a/internal/gen/controller/api/services/user_service_grpc.pb.go +++ b/internal/gen/controller/api/services/user_service_grpc.pb.go @@ -22,14 +22,15 @@ import ( const _ = grpc.SupportPackageIsVersion7 const ( - UserService_GetUser_FullMethodName = "/controller.api.services.v1.UserService/GetUser" - UserService_ListUsers_FullMethodName = "/controller.api.services.v1.UserService/ListUsers" - UserService_CreateUser_FullMethodName = "/controller.api.services.v1.UserService/CreateUser" - UserService_UpdateUser_FullMethodName = "/controller.api.services.v1.UserService/UpdateUser" - UserService_DeleteUser_FullMethodName = "/controller.api.services.v1.UserService/DeleteUser" - UserService_AddUserAccounts_FullMethodName = "/controller.api.services.v1.UserService/AddUserAccounts" - UserService_SetUserAccounts_FullMethodName = "/controller.api.services.v1.UserService/SetUserAccounts" - UserService_RemoveUserAccounts_FullMethodName = "/controller.api.services.v1.UserService/RemoveUserAccounts" + UserService_GetUser_FullMethodName = "/controller.api.services.v1.UserService/GetUser" + UserService_ListUsers_FullMethodName = "/controller.api.services.v1.UserService/ListUsers" + UserService_CreateUser_FullMethodName = "/controller.api.services.v1.UserService/CreateUser" + UserService_UpdateUser_FullMethodName = "/controller.api.services.v1.UserService/UpdateUser" + UserService_DeleteUser_FullMethodName = "/controller.api.services.v1.UserService/DeleteUser" + UserService_AddUserAccounts_FullMethodName = "/controller.api.services.v1.UserService/AddUserAccounts" + UserService_SetUserAccounts_FullMethodName = "/controller.api.services.v1.UserService/SetUserAccounts" + UserService_RemoveUserAccounts_FullMethodName = "/controller.api.services.v1.UserService/RemoveUserAccounts" + UserService_ListResolvableAliases_FullMethodName = "/controller.api.services.v1.UserService/ListResolvableAliases" ) // UserServiceClient is the client API for UserService service. @@ -84,6 +85,10 @@ type UserServiceClient interface { // will be removed from. If the provided Account ids is not associated with the // provided User, an error is returned. RemoveUserAccounts(ctx context.Context, in *RemoveUserAccountsRequest, opts ...grpc.CallOption) (*RemoveUserAccountsResponse, error) + // ListResolvableAliases returns a list of Aliases which point to a resource + // for which the provided user id has some permission. + // If missing or malformed an error is returned. + ListResolvableAliases(ctx context.Context, in *ListResolvableAliasesRequest, opts ...grpc.CallOption) (*ListResolvableAliasesResponse, error) } type userServiceClient struct { @@ -166,6 +171,15 @@ func (c *userServiceClient) RemoveUserAccounts(ctx context.Context, in *RemoveUs return out, nil } +func (c *userServiceClient) ListResolvableAliases(ctx context.Context, in *ListResolvableAliasesRequest, opts ...grpc.CallOption) (*ListResolvableAliasesResponse, error) { + out := new(ListResolvableAliasesResponse) + err := c.cc.Invoke(ctx, UserService_ListResolvableAliases_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // UserServiceServer is the server API for UserService service. // All implementations must embed UnimplementedUserServiceServer // for forward compatibility @@ -218,6 +232,10 @@ type UserServiceServer interface { // will be removed from. If the provided Account ids is not associated with the // provided User, an error is returned. RemoveUserAccounts(context.Context, *RemoveUserAccountsRequest) (*RemoveUserAccountsResponse, error) + // ListResolvableAliases returns a list of Aliases which point to a resource + // for which the provided user id has some permission. + // If missing or malformed an error is returned. + ListResolvableAliases(context.Context, *ListResolvableAliasesRequest) (*ListResolvableAliasesResponse, error) mustEmbedUnimplementedUserServiceServer() } @@ -249,6 +267,9 @@ func (UnimplementedUserServiceServer) SetUserAccounts(context.Context, *SetUserA func (UnimplementedUserServiceServer) RemoveUserAccounts(context.Context, *RemoveUserAccountsRequest) (*RemoveUserAccountsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RemoveUserAccounts not implemented") } +func (UnimplementedUserServiceServer) ListResolvableAliases(context.Context, *ListResolvableAliasesRequest) (*ListResolvableAliasesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListResolvableAliases not implemented") +} func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} // UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. @@ -406,6 +427,24 @@ func _UserService_RemoveUserAccounts_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _UserService_ListResolvableAliases_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListResolvableAliasesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).ListResolvableAliases(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_ListResolvableAliases_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).ListResolvableAliases(ctx, req.(*ListResolvableAliasesRequest)) + } + return interceptor(ctx, in, info, handler) +} + // UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -445,6 +484,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{ MethodName: "RemoveUserAccounts", Handler: _UserService_RemoveUserAccounts_Handler, }, + { + MethodName: "ListResolvableAliases", + Handler: _UserService_ListResolvableAliases_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "controller/api/services/v1/user_service.proto", diff --git a/internal/iam/repository_role_grant.go b/internal/iam/repository_role_grant.go index caff056610..b0d2a89a2a 100644 --- a/internal/iam/repository_role_grant.go +++ b/internal/iam/repository_role_grant.go @@ -403,7 +403,7 @@ func (r *Repository) ListRoleGrantScopes(ctx context.Context, roleId string, opt return roleGrantScopes, nil } -func (r *Repository) GrantsForUser(ctx context.Context, userId string, _ ...Option) ([]perms.GrantTuple, error) { +func (r *Repository) GrantsForUser(ctx context.Context, userId string, _ ...Option) (perms.GrantTuples, error) { const op = "iam.(Repository).GrantsForUser" if userId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id") diff --git a/internal/perms/acl.go b/internal/perms/acl.go index 17cc53e467..db006e3b36 100644 --- a/internal/perms/acl.go +++ b/internal/perms/acl.go @@ -276,6 +276,31 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option return } +// ListResolvablePermissions builds a set of Permissions based on the grants in +// the ACL. The permissions will only be created if there is at least +// one grant of the provided resource type that includes at least one of the +// provided actions in the action set. +// Note that unlike the ListPermissions method, this method does not attempt to +// generate permissions for the u_recovery user. To get the resolvable aliases +// for u_recovery, the user could simply query all aliases with a destination id. +func (a ACL) ListResolvablePermissions(requestedType resource.Type, actions action.ActionSet) []Permission { + perms := make([]Permission, 0, len(a.scopeMap)) + for scopeId := range a.scopeMap { + // Consider all scopes in the grants. They may not exist, but if that is + // the case the + p := Permission{ + ScopeId: scopeId, + Resource: requestedType, + Action: action.ListResolvableAliases, + OnlySelf: true, // default to only self to be restrictive + } + if a.buildPermission(scopeId, requestedType, actions, &p) { + perms = append(perms, p) + } + } + return perms +} + // ListPermissions builds a set of Permissions based on the grants in the ACL. // Permissions are determined for the given resource for each of the provided scopes. // There must be a grant for a given resource for one of the provided "id actions" @@ -301,55 +326,64 @@ func (a ACL) ListPermissions(requestedScopes map[string]*scopes.ScopeInfo, perms = append(perms, p) continue } + if a.buildPermission(scopeId, requestedType, idActions, &p) { + perms = append(perms, p) + } + } + return perms +} - // Get grants for a specific scope id from the source of truth. - grants := a.scopeMap[scopeId] - for _, grant := range grants { - // This grant doesn't match what we're looking for, ignore. - if grant.typ != requestedType && grant.typ != resource.All && globals.ResourceInfoFromPrefix(grant.id).Type != requestedType { - continue - } - - // We found a grant that matches the requested resource type: - // Search to see if one or all actions in the action set have been granted. - found := false - if ok := grant.actions[action.All]; ok { - found = true - } else { - for a := range idActions { - if ok := grant.actions[a]; ok { - found = true - break - } - } - } - if !found { // In this case, none of the requested actions were granted for the given scope id. - continue - } +// buildPermission populates the provided permission with either the resource ids +// or marking All to true if there are grants that have an action that match +// one of the provided idActions for the provided type +func (a ACL) buildPermission(scopeId string, + requestedType resource.Type, + idActions action.ActionSet, + p *Permission, +) bool { + // Get grants for a specific scope id from the source of truth. + grants := a.scopeMap[scopeId] + for _, grant := range grants { + // This grant doesn't match what we're looking for, ignore. + if grant.typ != requestedType && grant.typ != resource.All && globals.ResourceInfoFromPrefix(grant.id).Type != requestedType { + continue + } - actions, _ := grant.Actions() - excludeList := make(action.ActionSet, len(actions)) - for _, aa := range actions { - if aa != action.List { - excludeList.Add(aa) + // We found a grant that matches the requested resource type: + // Search to see if one or all actions in the action set have been granted. + found := false + if ok := grant.actions[action.All]; ok { + found = true + } else { + for idA := range idActions { + if ok := grant.actions[idA]; ok { + found = true + break } } - p.OnlySelf = p.OnlySelf && excludeList.OnlySelf() + } + if !found { // In this case, none of the requested actions were granted for the given scope id. + continue + } - switch grant.id { - case "*": - p.All = true - case "": - continue - default: - p.ResourceIds = append(p.ResourceIds, grant.id) + actions, _ := grant.Actions() + excludeList := make(action.ActionSet, len(actions)) + for _, aa := range actions { + if aa != action.List { + excludeList.Add(aa) } } + p.OnlySelf = p.OnlySelf && excludeList.OnlySelf() - if p.All || len(p.ResourceIds) > 0 { - perms = append(perms, p) + switch grant.id { + case "*": + p.All = true + case "": + continue + default: + p.ResourceIds = append(p.ResourceIds, grant.id) } } - return perms + return p.All || len(p.ResourceIds) > 0 } diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index 1dd4248c22..12d3423f3b 100644 --- a/internal/perms/acl_test.go +++ b/internal/perms/acl_test.go @@ -412,6 +412,367 @@ func Test_ACLAllowed(t *testing.T) { } } +func TestACL_ListResolvableAliasesPermissions(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + tests := []struct { + name string + aclGrants []scopeGrant + resourceType resource.Type + actionSet action.ActionSet + expPermissions []Permission + skipGrantValidationChecking bool + }{ + { + name: "Requested resource mismatch", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=target;actions=list,read"}, // List & Read for all Targets + }, + }, + resourceType: resource.Session, // We're requesting sessions. + actionSet: action.NewActionSet(action.Read), + expPermissions: []Permission{}, + }, + { + name: "Requested actions not available for the requested scope id", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=session;actions=delete"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.Read), + expPermissions: []Permission{}, + }, + { + name: "No specific id or wildcard provided for `id` field", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"type=*;actions=list,read"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.Read), + expPermissions: []Permission{}, + skipGrantValidationChecking: true, + }, + { + name: "Allow all ids", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=session;actions=update,read"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.Read), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, + }, + }, + }, + { + name: "Allow all ids, :self actions", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=session;actions=list,read:self"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.ReadSelf), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: true, + All: true, + }, + }, + }, + { + name: "Allow specific IDs", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{ + "ids=s_1;type=session;actions=list,read", + "ids=s_2,s_3;type=session;actions=list,read", + }, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.Read), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, + }, + }, + }, + { + name: "No specific type 1", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=*;actions=list,read:self"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.ReadSelf), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: true, + All: true, + }, + }, + }, + { + name: "List + No-op action with id wildcard", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=session;actions=list,no-op"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.NoOp), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, + }, + }, + }, + { + name: "List + No-op action with id wildcard, read present", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=session;actions=list,no-op"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.Read), + expPermissions: []Permission{}, + }, + { + name: "List + No-op action with specific ids", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{ + "ids=s_1;type=session;actions=list,no-op", + "ids=s_2,s_3;type=session;actions=list,no-op", + }, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.NoOp), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, + }, + }, + }, + { + name: "No specific type 2", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=*;actions=list,read:self"}, + }, + }, + resourceType: resource.Host, + actionSet: action.NewActionSet(action.ReadSelf), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Host, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: true, + All: true, + }, + }, + }, + { + name: "Grant hierarchy is respected", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{ + "ids=*;type=*;actions=*", + "ids=*;type=session;actions=cancel:self,list,read:self", + }, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.NoOp, action.Read, action.ReadSelf, action.Cancel, action.CancelSelf), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, + }, + }, + }, + { + name: "Full access 1", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=*;actions=*"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, + }, + }, + }, + { + name: "Full access 2", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{"ids=*;type=*;actions=*"}, + }, + }, + resourceType: resource.Host, + actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Host, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, + }, + }, + }, + { + name: "Multiple scopes", + aclGrants: []scopeGrant{ + { + scope: "o_1", + grants: []string{ + "ids=s_1;type=session;actions=create,read", + "ids=s_2,s_3;type=session;actions=update,read", + }, + }, + { + scope: "o_2", + grants: []string{"ids=*;type=session;actions=read:self"}, + }, + }, + resourceType: resource.Session, + actionSet: action.NewActionSet(action.Read, action.ReadSelf), + expPermissions: []Permission{ + { + ScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, + }, + { + ScopeId: "o_2", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: true, + All: true, + }, + }, + }, + { + name: "separate_type_id_resource_grants", + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + scope: "p_1", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + ScopeId: "p_1", + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var grants []Grant + for _, sg := range tt.aclGrants { + for _, g := range sg.grants { + grant, err := Parse(ctx, sg.scope, g, WithSkipFinalValidation(tt.skipGrantValidationChecking)) + require.NoError(t, err) + grants = append(grants, grant) + } + } + + acl := NewACL(grants...) + perms := acl.ListResolvablePermissions(tt.resourceType, tt.actionSet) + require.ElementsMatch(t, tt.expPermissions, perms) + }) + } +} + func TestACL_ListPermissions(t *testing.T) { t.Parallel() @@ -932,7 +1293,7 @@ func Test_AnonRestrictions(t *testing.T) { if i == resource.Controller || i == resource.Worker { continue } - for j := action.Type(1); j <= action.MonthlyActiveUsers; j++ { + for j := action.Type(1); j <= action.ListResolvableAliases; j++ { id := "foobar" prefixes := globals.ResourcePrefixesFromType(resource.Type(i)) if len(prefixes) > 0 { diff --git a/internal/perms/grants.go b/internal/perms/grants.go index b4f6ebbed4..eda5ed1fb9 100644 --- a/internal/perms/grants.go +++ b/internal/perms/grants.go @@ -5,8 +5,11 @@ package perms import ( "context" + "encoding/binary" "encoding/json" "fmt" + "hash" + "hash/fnv" "sort" "strings" "unicode" @@ -45,6 +48,57 @@ type GrantTuple struct { Grant string } +type GrantTuples []GrantTuple + +// GrantsHash returns a stable hash of all the grants in the GrantTuples. +func (g GrantTuples) GrantHash(ctx context.Context) ([]byte, error) { + const op = "perms.(GrantTuples).GrantHash" + var values []string + for _, grant := range g { + values = append(values, grant.Grant, grant.RoleId, grant.ScopeId) + } + // Sort for deterministic output + slices.Sort(values) + hashVal, err := hashStrings(values...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return binary.LittleEndian.AppendUint64(make([]byte, 0, 4), hashVal), nil +} + +func hashStrings(s ...string) (uint64, error) { + hasher := fnv.New64() + var h uint64 + var err error + for _, current := range s { + hasher.Reset() + if _, err = hasher.Write([]byte(current)); err != nil { + return 0, err + } + if h, err = hashUpdateOrdered(hasher, h, hasher.Sum64()); err != nil { + return 0, err + } + } + return h, nil +} + +// hashUpdateOrdered is taken directly from +// https://github.com/mitchellh/hashstructure +func hashUpdateOrdered(h hash.Hash64, a, b uint64) (uint64, error) { + // For ordered updates, use a real hash function + h.Reset() + + e1 := binary.Write(h, binary.LittleEndian, a) + e2 := binary.Write(h, binary.LittleEndian, b) + if e1 != nil { + return 0, e1 + } + if e2 != nil { + return 0, e2 + } + return h.Sum64(), nil +} + // Scope provides an in-memory representation of iam.Scope without the // underlying storage references or capabilities. type Scope struct { diff --git a/internal/proto/controller/api/services/v1/user_service.proto b/internal/proto/controller/api/services/v1/user_service.proto index 62067c9659..d996acd9bd 100644 --- a/internal/proto/controller/api/services/v1/user_service.proto +++ b/internal/proto/controller/api/services/v1/user_service.proto @@ -5,6 +5,7 @@ syntax = "proto3"; package controller.api.services.v1; +import "controller/api/resources/aliases/v1/alias.proto"; import "controller/api/resources/users/v1/user.proto"; import "google/api/annotations.proto"; import "google/protobuf/field_mask.proto"; @@ -126,6 +127,14 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {summary: "Removes the specified Accounts from being associated with the provided User."}; } + + // ListResolvableAliases returns a list of Aliases which point to a resource + // for which the provided user id has some permission. + // If missing or malformed an error is returned. + rpc ListResolvableAliases(ListResolvableAliasesRequest) returns (ListResolvableAliasesResponse) { + option (google.api.http) = {get: "/v1/users/{id}:list-resolvable-aliases"}; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {summary: "Lists all Aliases which point to a resource for which the requester has some permission."}; + } } message GetUserRequest { @@ -241,3 +250,48 @@ message RemoveUserAccountsRequest { message RemoveUserAccountsResponse { resources.users.v1.User item = 1; } + +message ListResolvableAliasesRequest { + string id = 1; // @gotags: `class:"public" eventstream:"observation"` + + // 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 = 2 [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 default page configured, + // an error will be returned. + uint32 page_size = 3 [json_name = "page_size"]; // @gotags: `class:"public"` +} + +message ListResolvableAliasesResponse { + repeated resources.aliases.v1.Alias 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. This includes aliases which have their + // destination_id removed or set to a resource for which the requester doesn't + // have permissions. + 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"` +} diff --git a/internal/types/action/action.go b/internal/types/action/action.go index da557a0c7d..dc6ca3f2d9 100644 --- a/internal/types/action/action.go +++ b/internal/types/action/action.go @@ -75,6 +75,7 @@ const ( SetGrantScopes Type = 61 RemoveGrantScopes Type = 62 MonthlyActiveUsers Type = 63 + ListResolvableAliases Type = 64 // When adding new actions, be sure to update: // @@ -146,6 +147,7 @@ var Map = map[string]Type{ SetGrantScopes.String(): SetGrantScopes, RemoveGrantScopes.String(): RemoveGrantScopes, MonthlyActiveUsers.String(): MonthlyActiveUsers, + ListResolvableAliases.String(): ListResolvableAliases, } var DeprecatedMap = map[string]Type{ @@ -223,6 +225,7 @@ func (a Type) String() string { "set-grant-scopes", "remove-grant-scopes", "monthly-active-users", + "list-resolvable-aliases", }[a] } diff --git a/internal/types/action/action_test.go b/internal/types/action/action_test.go index 8112d1e0e8..b0725224c4 100644 --- a/internal/types/action/action_test.go +++ b/internal/types/action/action_test.go @@ -138,6 +138,38 @@ func TestAction(t *testing.T) { action: Download, want: "download", }, + { + action: AttachStoragePolicy, + want: "attach-storage-policy", + }, + { + action: DetachStoragePolicy, + want: "detach-storage-policy", + }, + { + action: ReApplyStoragePolicy, + want: "reapply-storage-policy", + }, + { + action: AddGrantScopes, + want: "add-grant-scopes", + }, + { + action: SetGrantScopes, + want: "set-grant-scopes", + }, + { + action: RemoveGrantScopes, + want: "remove-grant-scopes", + }, + { + action: MonthlyActiveUsers, + want: "monthly-active-users", + }, + { + action: ListResolvableAliases, + want: "list-resolvable-aliases", + }, } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { From a51703f8049ddc2044e7f6509dafb08789dbe98f Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 8 Apr 2024 19:39:04 +0000 Subject: [PATCH 2/6] backport of commit 71f5cc8934c837937075d9038aeaca4f59d1200a --- internal/alias/target/service_list.go | 9 +- internal/alias/target/service_list_page.go | 5 +- internal/alias/target/service_list_refresh.go | 5 +- .../alias/target/service_list_refresh_page.go | 5 +- .../service_list_resolvable_ext_test.go | 345 ++++-------------- .../controller/handlers/users/user_service.go | 12 +- 6 files changed, 83 insertions(+), 298 deletions(-) diff --git a/internal/alias/target/service_list.go b/internal/alias/target/service_list.go index 5061f1b12e..fd2d01865c 100644 --- a/internal/alias/target/service_list.go +++ b/internal/alias/target/service_list.go @@ -64,7 +64,6 @@ func ListResolvableAliases( ctx context.Context, grantsHash []byte, pageSize int, - filterItemFn pagination.ListFilterFunc[*Alias], repo *Repository, permissions []perms.Permission, ) (*pagination.ListResponse[*Alias], error) { @@ -75,8 +74,6 @@ func ListResolvableAliases( 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(permissions) == 0: @@ -93,5 +90,9 @@ func ListResolvableAliases( return repo.listResolvableAliases(ctx, permissions, opts...) } - return pagination.List(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount) + return pagination.List(ctx, grantsHash, pageSize, alwaysTrueFilterFn, listItemsFn, repo.estimatedCount) +} + +func alwaysTrueFilterFn(context.Context, *Alias) (bool, error) { + return true, nil } diff --git a/internal/alias/target/service_list_page.go b/internal/alias/target/service_list_page.go index a21f22b6cc..f2a32b674b 100644 --- a/internal/alias/target/service_list_page.go +++ b/internal/alias/target/service_list_page.go @@ -82,7 +82,6 @@ func ListResolvableAliasesPage( ctx context.Context, grantsHash []byte, pageSize int, - filterItemFn pagination.ListFilterFunc[*Alias], tok *listtoken.Token, repo *Repository, perms []perms.Permission, @@ -94,8 +93,6 @@ func ListResolvableAliasesPage( 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: @@ -125,5 +122,5 @@ func ListResolvableAliasesPage( return repo.listResolvableAliases(ctx, perms, opts...) } - return pagination.ListPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, tok) + return pagination.ListPage(ctx, grantsHash, pageSize, alwaysTrueFilterFn, listItemsFn, repo.estimatedCount, tok) } diff --git a/internal/alias/target/service_list_refresh.go b/internal/alias/target/service_list_refresh.go index dff67f517c..7f284e89e9 100644 --- a/internal/alias/target/service_list_refresh.go +++ b/internal/alias/target/service_list_refresh.go @@ -91,7 +91,6 @@ func ListResolvableAliasesRefresh( ctx context.Context, grantsHash []byte, pageSize int, - filterItemFn pagination.ListFilterFunc[*Alias], tok *listtoken.Token, repo *Repository, permissions []perms.Permission, @@ -103,8 +102,6 @@ func ListResolvableAliasesRefresh( 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: @@ -136,5 +133,5 @@ func ListResolvableAliasesRefresh( return repo.listRemovedResolvableAliasIds(ctx, since.Add(-globals.RefreshReadLookbackDuration), permissions) } - return pagination.ListRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, listDeletedIdsFn, tok) + return pagination.ListRefresh(ctx, grantsHash, pageSize, alwaysTrueFilterFn, listItemsFn, repo.estimatedCount, listDeletedIdsFn, tok) } diff --git a/internal/alias/target/service_list_refresh_page.go b/internal/alias/target/service_list_refresh_page.go index cb15af563b..b1a4ccdc3d 100644 --- a/internal/alias/target/service_list_refresh_page.go +++ b/internal/alias/target/service_list_refresh_page.go @@ -99,7 +99,6 @@ func ListResolvableAliasesRefreshPage( ctx context.Context, grantsHash []byte, pageSize int, - filterItemFn pagination.ListFilterFunc[*Alias], tok *listtoken.Token, repo *Repository, permissions []perms.Permission, @@ -111,8 +110,6 @@ func ListResolvableAliasesRefreshPage( 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: @@ -151,5 +148,5 @@ func ListResolvableAliasesRefreshPage( return repo.listRemovedResolvableAliasIds(ctx, since.Add(-globals.RefreshReadLookbackDuration), permissions) } - return pagination.ListRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedCount, listDeletedIdsFn, tok) + return pagination.ListRefreshPage(ctx, grantsHash, pageSize, alwaysTrueFilterFn, listItemsFn, repo.estimatedCount, listDeletedIdsFn, tok) } diff --git a/internal/alias/target/service_list_resolvable_ext_test.go b/internal/alias/target/service_list_resolvable_ext_test.go index 4e03911b56..093d7e9c36 100644 --- a/internal/alias/target/service_list_resolvable_ext_test.go +++ b/internal/alias/target/service_list_resolvable_ext_test.go @@ -99,320 +99,204 @@ func TestService_ListResolvableAliases(t *testing.T) { t.Parallel() t.Run("missing grants hash", func(t *testing.T) { t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } - _, err := target.ListResolvableAliases(ctx, nil, 1, filterFunc, repo, byIdPerms) + _, err := target.ListResolvableAliases(ctx, nil, 1, repo, byIdPerms) require.ErrorContains(t, err, "missing grants hash") }) t.Run("zero page size", func(t *testing.T) { t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } - _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 0, filterFunc, repo, byIdPerms) + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 0, repo, byIdPerms) 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, r *target.Alias) (bool, error) { - return true, nil - } - _, err := target.ListResolvableAliases(ctx, []byte("some hash"), -1, filterFunc, repo, byIdPerms) + t.Parallel() + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), -1, repo, byIdPerms) require.ErrorContains(t, err, "page size must be at least 1") }) - t.Run("nil filter func", func(t *testing.T) { - t.Parallel() - _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) - require.ErrorContains(t, err, "missing filter item callback") - }) t.Run("nil repo", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } - _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, nil, byIdPerms) + t.Parallel() + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, nil, byIdPerms) require.ErrorContains(t, err, "missing repo") }) t.Run("missing target permissions", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } - _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, nil) + t.Parallel() + _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, nil) require.ErrorContains(t, err, "missing target permissions") }) }) 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, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesPage(ctx, nil, 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesPage(ctx, nil, 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "missing grants hash") }) t.Run("zero page size", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 0, tok, repo, byIdPerms) 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, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), -1, tok, repo, byIdPerms) 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.Alias, []byte("some hash"), "some-id", fiveDaysAgo) - require.NoError(t, err) - _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, nil, tok, repo, byIdPerms) - require.ErrorContains(t, err, "missing filter item callback") - }) t.Run("nil token", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } - _, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, byIdPerms) + t.Parallel() + _, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) require.ErrorContains(t, err, "missing token") }) t.Run("wrong token type", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a pagination token component") }) t.Run("nil repo", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, byIdPerms) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, tok, nil, byIdPerms) require.ErrorContains(t, err, "missing repo") }) t.Run("missing permissions", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, tok, repo, nil) require.ErrorContains(t, err, "missing permissions") }) t.Run("wrong token resource type", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a alias 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, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefresh(ctx, nil, 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefresh(ctx, nil, 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "missing grants hash") }) t.Run("zero page size", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 0, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 0, tok, repo, byIdPerms) 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, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), -1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), -1, tok, repo, byIdPerms) 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.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) - require.NoError(t, err) - _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, nil, tok, repo, byIdPerms) - require.ErrorContains(t, err, "missing filter item callback") - }) t.Run("nil token", func(t *testing.T) { t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } - _, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, nil, repo, byIdPerms) + + _, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) require.ErrorContains(t, err, "missing token") }) t.Run("wrong token type", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a start-refresh token component") }) t.Run("nil repo", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, nil, byIdPerms) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, tok, nil, byIdPerms) require.ErrorContains(t, err, "missing repo") }) t.Run("missing permissions", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, tok, repo, nil) require.ErrorContains(t, err, "missing target permissions") }) t.Run("wrong token resource type", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a alias 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, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefreshPage(ctx, nil, 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefreshPage(ctx, nil, 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "missing grants hash") }) t.Run("zero page size", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 0, tok, repo, byIdPerms) 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, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), -1, tok, repo, byIdPerms) 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.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) - require.NoError(t, err) - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, nil, tok, repo, byIdPerms) - require.ErrorContains(t, err, "missing filter item callback") - }) t.Run("nil token", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, byIdPerms) + t.Parallel() + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) require.ErrorContains(t, err, "missing token") }) t.Run("wrong token type", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a refresh token component") }) t.Run("nil repo", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, byIdPerms) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, tok, nil, byIdPerms) require.ErrorContains(t, err, "missing repo") }) t.Run("missing permissions", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, nil) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, tok, repo, nil) require.ErrorContains(t, err, "missing target permissions") }) t.Run("wrong token resource type", func(t *testing.T) { - t.Parallel() - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, byIdPerms) + _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a alias resource type") }) }) t.Run("simple pagination", func(t *testing.T) { - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } - cases := []struct { name string perms []perms.Permission @@ -431,7 +315,7 @@ func TestService_ListResolvableAliases(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, tc.perms) + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, tc.perms) require.NoError(t, err) require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) @@ -441,7 +325,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.Len(t, resp.Items, 1) require.Empty(t, cmp.Diff(resp.Items[0], tc.resourceSlice[0], cmpIgnoreUnexportedOpts), "resources did not match", tc.resourceSlice, "resp", resp.Items) - resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, tc.perms) + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, resp.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp2.CompleteListing) @@ -450,7 +334,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.Len(t, resp2.Items, 1) require.Empty(t, cmp.Diff(resp2.Items[0], tc.resourceSlice[1], cmpIgnoreUnexportedOpts)) - resp3, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, tc.perms) + resp3, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, resp2.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp3.CompleteListing) @@ -459,7 +343,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.Len(t, resp3.Items, 1) require.Empty(t, cmp.Diff(resp3.Items[0], tc.resourceSlice[2], cmpIgnoreUnexportedOpts)) - resp4, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, tc.perms) + resp4, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, resp3.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp4.CompleteListing) @@ -468,7 +352,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.Len(t, resp4.Items, 1) require.Empty(t, cmp.Diff(resp4.Items[0], tc.resourceSlice[3], cmpIgnoreUnexportedOpts)) - resp5, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, tc.perms) + resp5, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, resp4.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp5.CompleteListing) @@ -479,7 +363,7 @@ func TestService_ListResolvableAliases(t *testing.T) { // Finished initial pagination phase, request refresh // Expect no results. - resp6, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp5.ListToken, repo, tc.perms) + resp6, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, resp5.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp6.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp6.CompleteListing) @@ -504,7 +388,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) // Refresh again, should get newR2 - resp7, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp6.ListToken, repo, tc.perms) + resp7, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, resp6.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp7.CompleteListing) @@ -514,7 +398,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.Empty(t, cmp.Diff(resp7.Items[0], newR2, cmpIgnoreUnexportedOpts)) // Refresh again, should get newR1 - resp8, err := target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp7.ListToken, repo, tc.perms) + resp8, err := target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, resp7.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp8.CompleteListing) @@ -524,7 +408,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.Empty(t, cmp.Diff(resp8.Items[0], newR1, cmpIgnoreUnexportedOpts)) // Refresh again, should get no results - resp9, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp8.ListToken, repo, tc.perms) + resp9, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, resp8.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp9.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp9.CompleteListing) @@ -535,91 +419,7 @@ func TestService_ListResolvableAliases(t *testing.T) { } }) - t.Run("simple pagination with aggressive filtering", func(t *testing.T) { - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return r.GetPublicId() == byIdResources[1].GetPublicId() || - r.GetPublicId() == byIdResources[len(byIdResources)-1].GetPublicId(), nil - } - resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, byIdPerms) - 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, 10) - require.Empty(t, resp.DeletedIds) - require.Len(t, resp.Items, 1) - require.Empty(t, cmp.Diff(resp.Items[0], byIdResources[1], cmpIgnoreUnexportedOpts)) - - resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, byIdPerms) - 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, 10) - require.Empty(t, resp2.DeletedIds) - require.Len(t, resp2.Items, 1) - require.Empty(t, cmp.Diff(resp2.Items[0], byIdResources[len(byIdResources)-1], cmpIgnoreUnexportedOpts)) - - // request a refresh, nothing should be returned - resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, byIdPerms) - require.NoError(t, err) - require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) - require.True(t, resp3.CompleteListing) - require.Equal(t, resp3.EstimatedItemCount, 10) - require.Empty(t, resp3.DeletedIds) - require.Empty(t, resp3.Items) - - // Create some new aliases - newR1 := target.TestAlias(t, rw, "new.alias.one", target.WithDestinationId(tar.GetPublicId())) - newR2 := target.TestAlias(t, rw, "new.alias.two", target.WithDestinationId(tar.GetPublicId())) - newR3 := target.TestAlias(t, rw, "new.alias.three", target.WithDestinationId(tar.GetPublicId())) - newR4 := target.TestAlias(t, rw, "new.alias.four", target.WithDestinationId(tar.GetPublicId())) - // Run analyze to update count estimate - _, err = sqlDB.ExecContext(ctx, "analyze") - require.NoError(t, err) - t.Cleanup(func() { - _, err = repo.DeleteAlias(ctx, newR1.GetPublicId()) - require.NoError(t, err) - _, err = repo.DeleteAlias(ctx, newR2.GetPublicId()) - require.NoError(t, err) - _, err = repo.DeleteAlias(ctx, newR3.GetPublicId()) - require.NoError(t, err) - _, err = repo.DeleteAlias(ctx, newR4.GetPublicId()) - require.NoError(t, err) - // Run analyze to update count estimate - _, err = sqlDB.ExecContext(ctx, "analyze") - require.NoError(t, err) - }) - - filterFunc = func(_ context.Context, r *target.Alias) (bool, error) { - return r.GetPublicId() == newR3.GetPublicId() || - r.GetPublicId() == newR1.GetPublicId(), nil - } - // Refresh again, should get newR3 - resp4, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, byIdPerms) - require.NoError(t, err) - require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash")) - require.False(t, resp4.CompleteListing) - require.Equal(t, resp4.EstimatedItemCount, 14) - require.Empty(t, resp4.DeletedIds) - require.Len(t, resp4.Items, 1) - require.Empty(t, cmp.Diff(resp4.Items[0], newR3, cmpIgnoreUnexportedOpts)) - - // Refresh again, should get newR1 - resp5, err := target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, byIdPerms) - require.NoError(t, err) - require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash")) - require.True(t, resp5.CompleteListing) - require.Equal(t, resp5.EstimatedItemCount, 14) - require.Empty(t, resp5.DeletedIds) - require.Len(t, resp5.Items, 1) - require.Empty(t, cmp.Diff(resp5.Items[0], newR1, cmpIgnoreUnexportedOpts)) - }) - t.Run("simple pagination with destination id changes", func(t *testing.T) { - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } firstUpdatedA := byScopeResources[0] // this no longer has the destination id that has permissions firstUpdatedA.DestinationId = tar.GetPublicId() @@ -637,7 +437,7 @@ func TestService_ListResolvableAliases(t *testing.T) { _, err = sqlDB.ExecContext(ctx, "analyze") require.NoError(t, err) - resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, byScopePerms) + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, byScopePerms) require.NoError(t, err) require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) @@ -648,7 +448,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.Empty(t, cmp.Diff(resp.Items[0], byScopeResources[0], cmpIgnoreUnexportedOpts)) // request remaining results - resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 3, filterFunc, resp.ListToken, repo, byScopePerms) + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 3, resp.ListToken, repo, byScopePerms) require.NoError(t, err) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp2.CompleteListing) @@ -675,7 +475,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) // request a refresh, nothing should be returned except the deleted id - resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, byScopePerms) + resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, resp2.ListToken, repo, byScopePerms) require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp3.CompleteListing) @@ -685,9 +485,6 @@ func TestService_ListResolvableAliases(t *testing.T) { }) t.Run("simple pagination with deletion", func(t *testing.T) { - filterFunc := func(_ context.Context, r *target.Alias) (bool, error) { - return true, nil - } deletedAliasId := byIdResources[0].GetPublicId() _, err := repo.DeleteAlias(ctx, deletedAliasId) require.NoError(t, err) @@ -697,7 +494,7 @@ func TestService_ListResolvableAliases(t *testing.T) { _, err = sqlDB.ExecContext(ctx, "analyze") require.NoError(t, err) - resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, filterFunc, repo, byIdPerms) + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, byIdPerms) require.NoError(t, err) require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) @@ -708,7 +505,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.Empty(t, cmp.Diff(resp.Items[0], byIdResources[0], cmpIgnoreUnexportedOpts)) // request remaining results - resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 8, filterFunc, resp.ListToken, repo, byIdPerms) + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 8, resp.ListToken, repo, byIdPerms) require.NoError(t, err) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp2.CompleteListing) @@ -727,7 +524,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) // request a refresh, nothing should be returned except the deleted id - resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, byIdPerms) + resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, resp2.ListToken, repo, byIdPerms) require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp3.CompleteListing) diff --git a/internal/daemon/controller/handlers/users/user_service.go b/internal/daemon/controller/handlers/users/user_service.go index 7adc561511..6bf6af927c 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -528,10 +528,6 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva pageSize = int(req.GetPageSize()) } - filterItemFn := func(ctx context.Context, item *talias.Alias) (bool, error) { - return true, nil - } - repo, err := s.aliasRepoFn() if err != nil { return nil, errors.Wrap(ctx, err, op) @@ -540,7 +536,7 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva var sortBy string if req.GetListToken() == "" { sortBy = "created_time" - listResp, err = talias.ListResolvableAliases(ctx, grantsHash, pageSize, filterItemFn, repo, permissions) + listResp, err = talias.ListResolvableAliases(ctx, grantsHash, pageSize, repo, permissions) if err != nil { return nil, err } @@ -552,19 +548,19 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva switch st := listToken.Subtype.(type) { case *listtoken.PaginationToken: sortBy = "created_time" - listResp, err = talias.ListResolvableAliasesPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, permissions) + listResp, err = talias.ListResolvableAliasesPage(ctx, grantsHash, pageSize, listToken, repo, permissions) if err != nil { return nil, err } case *listtoken.StartRefreshToken: sortBy = "updated_time" - listResp, err = talias.ListResolvableAliasesRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, permissions) + listResp, err = talias.ListResolvableAliasesRefresh(ctx, grantsHash, pageSize, listToken, repo, permissions) if err != nil { return nil, err } case *listtoken.RefreshToken: sortBy = "updated_time" - listResp, err = talias.ListResolvableAliasesRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, permissions) + listResp, err = talias.ListResolvableAliasesRefreshPage(ctx, grantsHash, pageSize, listToken, repo, permissions) if err != nil { return nil, err } From 99fc2ed0b861eb2308b5ec77573f17c16fa2d14d Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 8 Apr 2024 19:46:02 +0000 Subject: [PATCH 3/6] backport of commit 8ef1d382bdb94c23a59f2740de670370b0cbf634 --- .../service_list_resolvable_ext_test.go | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/internal/alias/target/service_list_resolvable_ext_test.go b/internal/alias/target/service_list_resolvable_ext_test.go index 093d7e9c36..86a53bb15d 100644 --- a/internal/alias/target/service_list_resolvable_ext_test.go +++ b/internal/alias/target/service_list_resolvable_ext_test.go @@ -86,12 +86,12 @@ func TestService_ListResolvableAliases(t *testing.T) { // Reverse since we read items in descending order (newest first) slices.Reverse(byScopeResources) - repo, err := target.NewRepository(ctx, rw, rw, kmsCache) - require.NoError(t, err) + repo, repoErr := target.NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, repoErr) // Run analyze to update postgres estimates - _, err = sqlDB.ExecContext(ctx, "analyze") - require.NoError(t, err) + _, analyzeErr := sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, analyzeErr) cmpIgnoreUnexportedOpts := cmpopts.IgnoreUnexported(target.Alias{}, store.Alias{}, timestamp.Timestamp{}, timestamppb.Timestamp{}) @@ -108,17 +108,17 @@ func TestService_ListResolvableAliases(t *testing.T) { require.ErrorContains(t, err, "page size must be at least 1") }) t.Run("negative page size", func(t *testing.T) { - t.Parallel() + t.Parallel() _, err := target.ListResolvableAliases(ctx, []byte("some hash"), -1, repo, byIdPerms) require.ErrorContains(t, err, "page size must be at least 1") }) t.Run("nil repo", func(t *testing.T) { - t.Parallel() + t.Parallel() _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, nil, byIdPerms) require.ErrorContains(t, err, "missing repo") }) t.Run("missing target permissions", func(t *testing.T) { - t.Parallel() + t.Parallel() _, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, nil) require.ErrorContains(t, err, "missing target permissions") }) @@ -126,54 +126,54 @@ func TestService_ListResolvableAliases(t *testing.T) { t.Run("ListPage validation", func(t *testing.T) { t.Parallel() t.Run("missing grants hash", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesPage(ctx, nil, 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "missing grants hash") }) t.Run("zero page size", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 0, tok, repo, byIdPerms) require.ErrorContains(t, err, "page size must be at least 1") }) t.Run("negative page size", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), -1, tok, repo, byIdPerms) require.ErrorContains(t, err, "page size must be at least 1") }) t.Run("nil token", func(t *testing.T) { - t.Parallel() + t.Parallel() _, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) require.ErrorContains(t, err, "missing token") }) t.Run("wrong token type", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a pagination token component") }) t.Run("nil repo", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, tok, nil, byIdPerms) require.ErrorContains(t, err, "missing repo") }) t.Run("missing permissions", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, tok, repo, nil) require.ErrorContains(t, err, "missing permissions") }) t.Run("wrong token resource type", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) @@ -183,21 +183,21 @@ func TestService_ListResolvableAliases(t *testing.T) { t.Run("ListRefresh validation", func(t *testing.T) { t.Parallel() t.Run("missing grants hash", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefresh(ctx, nil, 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "missing grants hash") }) t.Run("zero page size", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 0, tok, repo, byIdPerms) require.ErrorContains(t, err, "page size must be at least 1") }) t.Run("negative page size", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), -1, tok, repo, byIdPerms) @@ -205,33 +205,33 @@ func TestService_ListResolvableAliases(t *testing.T) { }) t.Run("nil token", func(t *testing.T) { t.Parallel() - + _, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) require.ErrorContains(t, err, "missing token") }) t.Run("wrong token type", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a start-refresh token component") }) t.Run("nil repo", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, tok, nil, byIdPerms) require.ErrorContains(t, err, "missing repo") }) t.Run("missing permissions", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, tok, repo, nil) require.ErrorContains(t, err, "missing target permissions") }) t.Run("wrong token resource type", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) @@ -241,54 +241,54 @@ func TestService_ListResolvableAliases(t *testing.T) { t.Run("ListRefreshPage validation", func(t *testing.T) { t.Parallel() t.Run("missing grants hash", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefreshPage(ctx, nil, 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "missing grants hash") }) t.Run("zero page size", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 0, tok, repo, byIdPerms) require.ErrorContains(t, err, "page size must be at least 1") }) t.Run("negative page size", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), -1, tok, repo, byIdPerms) require.ErrorContains(t, err, "page size must be at least 1") }) t.Run("nil token", func(t *testing.T) { - t.Parallel() - _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) + t.Parallel() + _, err := target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, nil, repo, byIdPerms) require.ErrorContains(t, err, "missing token") }) t.Run("wrong token type", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), "some-id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) require.ErrorContains(t, err, "token did not have a refresh token component") }) t.Run("nil repo", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, tok, nil, byIdPerms) require.ErrorContains(t, err, "missing repo") }) t.Run("missing permissions", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Alias, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, tok, repo, nil) require.ErrorContains(t, err, "missing target permissions") }) t.Run("wrong token resource type", func(t *testing.T) { - t.Parallel() + t.Parallel() tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo) require.NoError(t, err) _, err = target.ListResolvableAliasesRefreshPage(ctx, []byte("some hash"), 1, tok, repo, byIdPerms) @@ -375,7 +375,7 @@ func TestService_ListResolvableAliases(t *testing.T) { newR1 := target.TestAlias(t, rw, "first.new.alias", target.WithDestinationId(tc.resourceSlice[0].GetDestinationId())) newR2 := target.TestAlias(t, rw, "second.new.alias", target.WithDestinationId(tc.resourceSlice[0].GetDestinationId())) t.Cleanup(func() { - _, err = repo.DeleteAlias(ctx, newR1.GetPublicId()) + _, err := repo.DeleteAlias(ctx, newR1.GetPublicId()) require.NoError(t, err) _, err = repo.DeleteAlias(ctx, newR2.GetPublicId()) require.NoError(t, err) @@ -423,12 +423,12 @@ func TestService_ListResolvableAliases(t *testing.T) { firstUpdatedA := byScopeResources[0] // this no longer has the destination id that has permissions firstUpdatedA.DestinationId = tar.GetPublicId() - firstUpdatedA, _, err = repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) + firstUpdatedA, _, err := repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) require.NoError(t, err) byScopeResources = byScopeResources[1:] t.Cleanup(func() { firstUpdatedA.DestinationId = tar2.GetPublicId() - firstUpdatedA, _, err = repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) + firstUpdatedA, _, err := repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) require.NoError(t, err) byScopeResources = append([]*target.Alias{firstUpdatedA}, byScopeResources...) }) @@ -465,7 +465,7 @@ func TestService_ListResolvableAliases(t *testing.T) { byScopeResources = byScopeResources[1:] t.Cleanup(func() { secondA.DestinationId = tar2.GetPublicId() - secondA, _, err = repo.UpdateAlias(ctx, secondA, secondA.GetVersion(), []string{"DestinationId"}) + secondA, _, err := repo.UpdateAlias(ctx, secondA, secondA.GetVersion(), []string{"DestinationId"}) require.NoError(t, err) byScopeResources = append([]*target.Alias{secondA}, byScopeResources...) }) From 4b58827bc5d41dc3b557cb3ec23b1cda4ebde38c Mon Sep 17 00:00:00 2001 From: Todd Date: Tue, 16 Apr 2024 16:31:58 +0000 Subject: [PATCH 4/6] backport of commit e24f8cb05e9af392b1f5534d69e12a568ae40c02 --- internal/daemon/controller/handlers/users/user_service.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/daemon/controller/handlers/users/user_service.go b/internal/daemon/controller/handlers/users/user_service.go index 6bf6af927c..3956bfc4ed 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -507,8 +507,7 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva } } - permissions := acl.ListResolvablePermissions(resource.Target, - action.Difference(targets.IdActions, action.NewActionSet(action.NoOp))) + permissions := acl.ListResolvablePermissions(resource.Target, targets.IdActions) if len(permissions) == 0 { // if there are no permitted targets then there will be no aliases that From ee79d52c8203389a6e49eca42c0eb02d46f478c0 Mon Sep 17 00:00:00 2001 From: Todd Date: Tue, 16 Apr 2024 20:29:19 +0000 Subject: [PATCH 5/6] backport of commit 28a94d6623ef34bfdbed246bcdef7d3e987f587e --- internal/daemon/controller/handlers/users/user_service.go | 2 -- internal/perms/grants.go | 8 ++++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/daemon/controller/handlers/users/user_service.go b/internal/daemon/controller/handlers/users/user_service.go index 3956bfc4ed..8ca5ac2852 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -15,7 +15,6 @@ import ( "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets" "github.com/hashicorp/boundary/internal/errors" - "github.com/hashicorp/boundary/internal/event" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/iam/store" @@ -518,7 +517,6 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva SortDir: "desc", }, nil } - event.WriteSysEvent(ctx, op, "permissions found") pageSize := int(s.maxPageSize) // Use the requested page size only if it is smaller than diff --git a/internal/perms/grants.go b/internal/perms/grants.go index eda5ed1fb9..de73502d0d 100644 --- a/internal/perms/grants.go +++ b/internal/perms/grants.go @@ -59,14 +59,18 @@ func (g GrantTuples) GrantHash(ctx context.Context) ([]byte, error) { } // Sort for deterministic output slices.Sort(values) - hashVal, err := hashStrings(values...) + hashVal, err := hashStrings(ctx, values...) if err != nil { return nil, errors.Wrap(ctx, err, op) } return binary.LittleEndian.AppendUint64(make([]byte, 0, 4), hashVal), nil } -func hashStrings(s ...string) (uint64, error) { +func hashStrings(ctx context.Context, s ...string) (uint64, error) { + const op = "perms.hashStrings" + if len(s) == 0 { + return 0, errors.New(ctx, errors.InvalidParameter, op, "no strings provided") + } hasher := fnv.New64() var h uint64 var err error From 9cd467e9968f25ae37ad31757cbcfa7bc31708ae Mon Sep 17 00:00:00 2001 From: Todd Date: Tue, 16 Apr 2024 22:00:51 +0000 Subject: [PATCH 6/6] backport of commit ce35e2209047c4ec7114a4db789d32ce64a0e923 --- internal/perms/grants.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/internal/perms/grants.go b/internal/perms/grants.go index de73502d0d..fa8a061356 100644 --- a/internal/perms/grants.go +++ b/internal/perms/grants.go @@ -53,24 +53,21 @@ type GrantTuples []GrantTuple // GrantsHash returns a stable hash of all the grants in the GrantTuples. func (g GrantTuples) GrantHash(ctx context.Context) ([]byte, error) { const op = "perms.(GrantTuples).GrantHash" + // TODO: Should this return an error when the GrantTuples is empty? var values []string for _, grant := range g { values = append(values, grant.Grant, grant.RoleId, grant.ScopeId) } // Sort for deterministic output slices.Sort(values) - hashVal, err := hashStrings(ctx, values...) + hashVal, err := hashStrings(values...) if err != nil { return nil, errors.Wrap(ctx, err, op) } return binary.LittleEndian.AppendUint64(make([]byte, 0, 4), hashVal), nil } -func hashStrings(ctx context.Context, s ...string) (uint64, error) { - const op = "perms.hashStrings" - if len(s) == 0 { - return 0, errors.New(ctx, errors.InvalidParameter, op, "no strings provided") - } +func hashStrings(s ...string) (uint64, error) { hasher := fnv.New64() var h uint64 var err error