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) {