From 0d67b86543aa82fb885955bb1535fd81d8e155dd Mon Sep 17 00:00:00 2001 From: Todd Date: Wed, 17 Apr 2024 08:26:46 -0700 Subject: [PATCH] Add ListResolvableAliases to the user service (#4609) * Add ListResolvableAliases to the user service There are a few non common ways this method uses our ACL and grant system. A user is able to list resolvable aliases for another user if they are granted permission to do so. That means we had to load the grants for the user being listed for and not the requester when determining if an alias is able to resolve to a destination for which the requested user has permission. Similarly, the grant hash used for the list pagination is required to be that of the user being listed and not the requester. --- .../repository_alias_list_resolvable.go | 220 +++++ internal/alias/target/service_list.go | 45 + internal/alias/target/service_list_page.go | 56 ++ internal/alias/target/service_list_refresh.go | 61 ++ .../alias/target/service_list_refresh_page.go | 69 ++ .../service_list_resolvable_ext_test.go | 535 ++++++++++++ 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 | 218 ++++- .../handlers/users/user_service_test.go | 434 +++++++++- .../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 | 128 +-- internal/perms/acl_test.go | 363 +++++++- internal/perms/grants.go | 55 ++ .../api/services/v1/user_service.proto | 54 ++ internal/types/action/action.go | 3 + internal/types/action/action_test.go | 32 + 26 files changed, 3090 insertions(+), 444 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 000000000..da78507ea --- /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 19f808f25..fd2d01865 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,47 @@ 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, + 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 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, 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 cb5d70d24..f2a32b674 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,58 @@ 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, + 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 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, alwaysTrueFilterFn, listItemsFn, repo.estimatedCount, tok) +} diff --git a/internal/alias/target/service_list_refresh.go b/internal/alias/target/service_list_refresh.go index 67ae7f43e..7f284e89e 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,63 @@ 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, + 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 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, 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 417cbf036..b1a4ccdc3 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,71 @@ 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, + 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 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, 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 new file mode 100644 index 000000000..86a53bb15 --- /dev/null +++ b/internal/alias/target/service_list_resolvable_ext_test.go @@ -0,0 +1,535 @@ +// 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, repoErr := target.NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, repoErr) + + // Run analyze to update postgres estimates + _, analyzeErr := sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, analyzeErr) + + 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() + _, 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() + _, 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() + _, 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() + _, 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() + _, 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() + 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() + 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() + 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() + _, 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() + 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() + 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() + 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() + 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) + 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() + 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() + 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() + 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) + require.ErrorContains(t, err, "page size must be at least 1") + }) + 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() + 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() + 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() + 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() + 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) + 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() + 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() + 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() + 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) + require.ErrorContains(t, err, "missing token") + }) + t.Run("wrong token type", 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.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() + 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() + 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() + 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) + require.ErrorContains(t, err, "token did not have a alias resource type") + }) + }) + + t.Run("simple pagination", func(t *testing.T) { + 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 destination id changes", func(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"}) + 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, 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, 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, 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) { + 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, 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, 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, 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 d559281e7..87dc30a10 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 7689ce5cb..869f83564 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 56817b541..06c4efced 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 477c23254..8ca5ac285 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -8,10 +8,12 @@ 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" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" @@ -23,6 +25,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 +46,7 @@ var ( action.AddAccounts, action.SetAccounts, action.RemoveAccounts, + action.ListResolvableAliases, ) // CollectionActions contains the set of actions that can be performed on @@ -71,21 +75,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 +476,163 @@ 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, targets.IdActions) + + 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 + } + + 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()) + } + + 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, 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, listToken, repo, permissions) + if err != nil { + return nil, err + } + case *listtoken.StartRefreshToken: + sortBy = "updated_time" + 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, 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 +888,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 +1048,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 f1e6a41cf..1a964f07c 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 TestDelete(t *testing.T) { - u, _, repoFn := createDefaultUserAndRepo(t, false) +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) - s, err := users.NewService(context.Background(), repoFn, 1000) + 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, aliasRepoFn := createDefaultUserAndRepos(t, false) + + 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 ec5b6214f..d899476f2 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 756c49a1a..dac2c5e1e 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 32686920d..e61db2e47 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 033b37f9d..5a2b452c8 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 efb731c8f..ea4a405e2 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 7177d6e63..664ca9aae 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, + 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, 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, 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, + 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, 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, + 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 13eb1fe09..68033362f 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 5859f37b8..ca2538c71 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 caff05661..b0d2a89a2 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 17cc53e46..db006e3b3 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 } - - // 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 - } - - 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() - - switch grant.id { - case "*": - p.All = true - case "": - continue - default: - p.ResourceIds = append(p.ResourceIds, grant.id) - } - } - - if p.All || len(p.ResourceIds) > 0 { + if a.buildPermission(scopeId, requestedType, idActions, &p) { perms = append(perms, p) } } - return perms } + +// 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 + } + + // 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 + } + } + } + if !found { // In this case, none of the requested actions were granted for the given scope id. + continue + } + + 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() + + switch grant.id { + case "*": + p.All = true + case "": + continue + default: + p.ResourceIds = append(p.ResourceIds, grant.id) + } + } + + return p.All || len(p.ResourceIds) > 0 +} diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index 1dd4248c2..12d3423f3 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 b4f6ebbed..fa8a06135 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,58 @@ 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" + // 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(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 62067c965..d996acd9b 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 da557a0c7..dc6ca3f2d 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 8112d1e0e..b0725224c 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) {