Paginate iam user (#4185)

* paginate iam user
This commit is contained in:
Todd authored and Johan Brandhorst-Satzkorn committed 2024-01-05 07:43:21 -08:00
1 parent 69a6f664de
commit 0fad770d8a
21 files changed
+2242 -339

No files matched your search

+1 -1
View File
@@ -185,7 +185,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)
us, err := users.NewService(c.baseContext, c.IamRepoFn, c.conf.RawConfig.Controller.MaxPageSize)
if err != nil {
return fmt.Errorf("failed to create user handler service: %w", err)
}
@@ -16,11 +16,14 @@ import (
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"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"
"google.golang.org/grpc/codes"
@@ -67,22 +70,27 @@ func init() {
type Service struct {
pbs.UnsafeUserServiceServer
repoFn common.IamRepoFactory
repoFn common.IamRepoFactory
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) (Service, error) {
func NewService(ctx context.Context, repo common.IamRepoFactory, maxPageSize uint) (Service, error) {
const op = "users.NewService"
if repo == nil {
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository")
}
return Service{repoFn: repo}, nil
if maxPageSize == 0 {
maxPageSize = uint(globals.DefaultMaxPageSize)
}
return Service{repoFn: repo, maxPageSize: maxPageSize}, nil
}
// ListUsers implements the interface pbs.UserServiceServer.
func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs.ListUsersResponse, error) {
const op = "users.(Service).ListUsers"
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
@@ -110,50 +118,115 @@ func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs
return &pbs.ListUsersResponse{}, nil
}
ul, err := s.listFromRepo(ctx, scopeIds)
if err != nil {
return nil, err
}
if len(ul) == 0 {
return &pbs.ListUsersResponse{}, 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())
}
filter, err := handlers.NewFilter(ctx, req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.User, 0, len(ul))
res := perms.Resource{
Type: resource.User,
}
for _, item := range ul {
res.Id = item.GetPublicId()
res.ScopeId = item.GetScopeId()
authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings()
if len(authorizedActions) == 0 {
continue
}
outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(*authResults.UserData.User.Id)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(scopeInfoMap[item.GetScopeId()]))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions))
}
item, err := toProto(ctx, item, nil, outputOpts...)
var filterItemFn func(ctx context.Context, item *iam.User) (bool, error)
switch {
case req.GetFilter() != "":
// Only use a filter if we need to
filter, err := handlers.NewFilter(ctx, req.GetFilter())
if err != nil {
return nil, err
}
if filter.Match(item) {
finalItems = append(finalItems, item)
filterItemFn = func(ctx context.Context, item *iam.User) (bool, error) {
outputOpts, ok := newOutputOpts(ctx, item, scopeInfoMap, authResults)
if !ok {
return false, nil
}
pbItem, err := toProto(ctx, item, nil, outputOpts...)
if err != nil {
return false, err
}
return filter.Match(pbItem), nil
}
default:
filterItemFn = func(ctx context.Context, item *iam.User) (bool, error) {
return true, nil
}
}
return &pbs.ListUsersResponse{Items: finalItems}, nil
grantsHash, err := authResults.GrantsHash(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
repo, err := s.repoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var listResp *pagination.ListResponse[*iam.User]
var sortBy string
if req.GetListToken() == "" {
sortBy = "created_time"
listResp, err = iam.ListUsers(ctx, grantsHash, pageSize, filterItemFn, repo, scopeIds)
if err != nil {
return nil, err
}
} else {
listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.User, grantsHash)
if err != nil {
return nil, err
}
switch st := listToken.Subtype.(type) {
case *listtoken.PaginationToken:
sortBy = "created_time"
listResp, err = iam.ListUsersPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds)
if err != nil {
return nil, err
}
case *listtoken.StartRefreshToken:
sortBy = "updated_time"
listResp, err = iam.ListUsersRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds)
if err != nil {
return nil, err
}
case *listtoken.RefreshToken:
sortBy = "updated_time"
listResp, err = iam.ListUsersRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds)
if err != nil {
return nil, err
}
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st)
}
}
finalItems := make([]*pb.User, 0, len(listResp.Items))
for _, item := range listResp.Items {
outputOpts, ok := newOutputOpts(ctx, item, scopeInfoMap, authResults)
if !ok {
continue
}
item, err := toProto(ctx, item, nil, outputOpts...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
finalItems = append(finalItems, item)
}
respType := "delta"
if listResp.CompleteListing {
respType = "complete"
}
resp := &pbs.ListUsersResponse{
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_USER)
if err != nil {
return nil, err
}
}
return resp, nil
}
// GetUsers implements the interface pbs.UserServiceServer.
@@ -493,18 +566,6 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) {
return rows > 0, nil
}
func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*iam.User, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
ul, err := repo.ListUsers(ctx, scopeIds, iam.WithLimit(-1))
if err != nil {
return nil, err
}
return ul, nil
}
func (s Service) addInRepo(ctx context.Context, userId string, accountIds []string, version uint32) (*iam.User, []string, error) {
const op = "users.(Service).addInRepo"
repo, err := s.repoFn()
@@ -792,3 +853,26 @@ func validateRemoveUserAccountsRequest(req *pbs.RemoveUserAccountsRequest) error
}
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,
}
res.Id = item.GetPublicId()
res.ScopeId = item.GetScopeId()
authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res))
if len(authorizedActions) == 0 {
return nil, false
}
outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(scopeInfoMap[item.GetScopeId()]))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions.Strings()))
}
return outputOpts, true
}
@@ -7,7 +7,7 @@ import (
"context"
"errors"
"fmt"
"sort"
"slices"
"strings"
"testing"
@@ -17,13 +17,17 @@ import (
"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/handlers"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/users"
"github.com/hashicorp/boundary/internal/db"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/users"
@@ -145,7 +149,7 @@ func TestGet(t *testing.T) {
req := proto.Clone(toMerge).(*pbs.GetUserRequest)
proto.Merge(req, tc.req)
s, err := users.NewService(context.Background(), repoFn)
s, err := users.NewService(context.Background(), repoFn, 1000)
require.NoError(err, "Couldn't create new user service.")
got, gErr := s.GetUser(auth.DisabledAuthTestContext(repoFn, u.GetScopeId()), req)
@@ -166,6 +170,7 @@ func TestGet(t *testing.T) {
}
func TestList(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrap := db.TestWrapper(t)
iamRepo := iam.TestRepo(t, conn, wrap)
@@ -191,23 +196,25 @@ func TestList(t *testing.T) {
secondaryAm := password.TestAuthMethods(t, conn, oWithUsers.PublicId, 1)
require.Len(t, secondaryAm, 1)
s, err := users.NewService(context.Background(), repoFn)
s, err := users.NewService(context.Background(), repoFn, 1000)
require.NoError(t, err)
var wantUsers []*pb.User
// Populate expected values for recursive test
var totalUsers []*pb.User
ctx := auth.DisabledAuthTestContext(repoFn, "global")
anon, err := s.GetUser(ctx, &pbs.GetUserRequest{Id: globals.AnonymousUserId})
require.NoError(t, err)
totalUsers = append(totalUsers, anon.GetItem())
authUser, err := s.GetUser(ctx, &pbs.GetUserRequest{Id: globals.AnyAuthenticatedUserId})
require.NoError(t, err)
totalUsers = append(totalUsers, authUser.GetItem())
recovery, err := s.GetUser(ctx, &pbs.GetUserRequest{Id: globals.RecoveryUserId})
require.NoError(t, err)
totalUsers = append(totalUsers, recovery.GetItem())
{
disabledAuthCtx := auth.DisabledAuthTestContext(repoFn, "global")
anon, err := s.GetUser(disabledAuthCtx, &pbs.GetUserRequest{Id: globals.AnonymousUserId})
require.NoError(t, err)
totalUsers = append(totalUsers, anon.GetItem())
authUser, err := s.GetUser(disabledAuthCtx, &pbs.GetUserRequest{Id: globals.AnyAuthenticatedUserId})
require.NoError(t, err)
totalUsers = append(totalUsers, authUser.GetItem())
recovery, err := s.GetUser(disabledAuthCtx, &pbs.GetUserRequest{Id: globals.RecoveryUserId})
require.NoError(t, err)
totalUsers = append(totalUsers, recovery.GetItem())
}
// Add new users
for i := 0; i < 10; i++ {
@@ -238,12 +245,17 @@ func TestList(t *testing.T) {
PrimaryAccountId: oidcAcct.GetPublicId(),
})
}
// Populate these users into the total
ctx = auth.DisabledAuthTestContext(repoFn, oWithUsers.GetPublicId(), auth.WithUserId(globals.AnyAuthenticatedUserId))
usersInOrg, err := s.ListUsers(ctx, &pbs.ListUsersRequest{ScopeId: oWithUsers.GetPublicId()})
totalUsers = append(totalUsers, wantUsers...)
slices.Reverse(totalUsers)
slices.Reverse(wantUsers)
// Run analyze to update postgres estimates
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
totalUsers = append(totalUsers, usersInOrg.GetItems()...)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
cases := []struct {
name string
req *pbs.ListUsersRequest
@@ -253,32 +265,64 @@ func TestList(t *testing.T) {
{
name: "List Many Users",
req: &pbs.ListUsersRequest{ScopeId: oWithUsers.GetPublicId()},
res: &pbs.ListUsersResponse{Items: wantUsers},
res: &pbs.ListUsersResponse{
Items: wantUsers,
EstItemCount: uint32(len(wantUsers)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List No Users",
req: &pbs.ListUsersRequest{ScopeId: oNoUsers.GetPublicId()},
res: &pbs.ListUsersResponse{},
res: &pbs.ListUsersResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List Recursively in Org",
req: &pbs.ListUsersRequest{ScopeId: oWithUsers.GetPublicId(), Recursive: true},
res: &pbs.ListUsersResponse{Items: wantUsers},
res: &pbs.ListUsersResponse{
Items: wantUsers,
EstItemCount: uint32(len(wantUsers)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List Recursively in Global",
req: &pbs.ListUsersRequest{ScopeId: "global", Recursive: true},
res: &pbs.ListUsersResponse{Items: totalUsers},
res: &pbs.ListUsersResponse{
Items: totalUsers,
EstItemCount: uint32(len(totalUsers)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter Many Users",
req: &pbs.ListUsersRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithUsers.GetPublicId())},
res: &pbs.ListUsersResponse{Items: wantUsers},
res: &pbs.ListUsersResponse{
Items: wantUsers,
EstItemCount: uint32(len(wantUsers)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter To No Users",
req: &pbs.ListUsersRequest{ScopeId: oWithUsers.GetPublicId(), Filter: `"/item/id"=="doesntmatch"`},
res: &pbs.ListUsersResponse{},
res: &pbs.ListUsersResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter Bad Format",
@@ -298,20 +342,6 @@ func TestList(t *testing.T) {
assert.True(errors.Is(gErr, tc.err), "ListUsers(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
return
}
gotUsers := sortableUsers{
users: got.GetItems(),
}
resUsers := sortableUsers{
users: tc.res.GetItems(),
}
if got != nil {
sort.Sort(gotUsers)
got.Items = gotUsers.users
}
if tc.res != nil {
sort.Sort(resUsers)
tc.res.Items = resUsers.users
}
assert.Empty(cmp.Diff(
got,
tc.res,
@@ -319,6 +349,7 @@ func TestList(t *testing.T) {
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.IgnoreFields(&pbs.ListUsersResponse{}, "list_token"),
), "ListUsers(%q) got response %q, wanted %q", tc.req, got, tc.res)
// Test with anon user
@@ -336,18 +367,372 @@ func TestList(t *testing.T) {
}
}
type sortableUsers struct {
users []*pb.User
func userToProto(u *iam.User, si *scopes.ScopeInfo, authorizedActions []string) *pb.User {
pu := &pb.User{
Id: u.GetPublicId(),
ScopeId: u.GetScopeId(),
Scope: si,
CreatedTime: u.GetCreateTime().GetTimestamp(),
UpdatedTime: u.GetUpdateTime().GetTimestamp(),
Version: u.GetVersion(),
AuthorizedActions: testAuthorizedActions,
}
if u.GetName() != "" {
pu.Name = wrapperspb.String(u.GetName())
}
if u.GetDescription() != "" {
pu.Description = wrapperspb.String(u.GetDescription())
}
return pu
}
func (s sortableUsers) Len() int { return len(s.users) }
func (s sortableUsers) Less(i, j int) bool { return s.users[i].GetId() < s.users[j].GetId() }
func (s sortableUsers) Swap(i, j int) { s.users[i], s.users[j] = s.users[j], s.users[i] }
func TestListPagination(t *testing.T) {
// Set database read timeout to avoid duplicates in response
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
sqlDB, err := conn.SqlDB(ctx)
require.NoError(t, err)
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
rw := db.New(conn)
iamRepo := iam.TestRepo(t, conn, wrapper)
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
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)
oNoUsers, _ := iam.TestScopes(t, iamRepo)
oWithUsers, _ := iam.TestScopes(t, iamRepo)
var allUsers []*pb.User
// Get the 3 system users (u_recovery, u_anon, u_auth)
us, _, err := iamRepo.ListUsers(ctx, []string{"global"})
require.NoError(t, err)
require.Len(t, us, 3)
// They (should) be returned in reverse order by create time, so we reverse
slices.Reverse(us)
for _, u := range us {
allUsers = append(allUsers, userToProto(u, &scopes.ScopeInfo{
Id: u.ScopeId,
Name: scope.Global.String(),
Description: "Global Scope",
Type: scope.Global.String(),
}, testAuthorizedActions))
}
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))
allUsers = append(allUsers, userToProto(u, &scopes.ScopeInfo{
Id: u.ScopeId,
Name: scope.Global.String(),
Description: "Global Scope",
Type: scope.Global.String(),
}, testAuthorizedActions))
// add roles to be able to see all users
allowedRole := iam.TestRole(t, conn, "global")
iam.TestRoleGrant(t, conn, allowedRole.GetPublicId(), "id=*;type=*;actions=*")
iam.TestUserRole(t, conn, allowedRole.GetPublicId(), u.GetPublicId())
for _, scope := range []*iam.Scope{oWithUsers, oNoUsers} {
allowedRole := iam.TestRole(t, conn, scope.GetPublicId())
iam.TestRoleGrant(t, conn, allowedRole.GetPublicId(), "id=*;type=*;actions=*")
iam.TestUserRole(t, conn, allowedRole.GetPublicId(), u.GetPublicId())
}
at, err := tokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId())
require.NoError(t, err)
// Test without anon user
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)
var safeToDeleteUser string
orgScopeInfo := &scopes.ScopeInfo{Id: oWithUsers.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}
for i := 0; i < 10; i++ {
ou := iam.TestUser(t, iamRepo, oWithUsers.GetPublicId())
allUsers = append(allUsers, userToProto(ou, orgScopeInfo, testAuthorizedActions))
safeToDeleteUser = ou.GetPublicId()
}
slices.Reverse(allUsers)
a, err := users.NewService(ctx, iamRepoFn, 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)
itemCount := uint32(len(allUsers))
testPageSize := int((itemCount - 2) / 2)
// Start paginating, recursively
req := &pbs.ListUsersRequest{
ScopeId: "global",
Recursive: true,
Filter: "",
ListToken: "",
PageSize: uint32(testPageSize),
}
got, err := a.ListUsers(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.ListUsersResponse{
Items: allUsers[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.ListUsersResponse{}, "list_token"),
),
)
// Request second page
req.ListToken = got.ListToken
got, err = a.ListUsers(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.ListUsersResponse{
Items: allUsers[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.ListUsersResponse{}, "list_token"),
),
)
// Request rest of results
req.ListToken = got.ListToken
req.PageSize = 10
got, err = a.ListUsers(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.ListUsersResponse{
Items: allUsers[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.ListUsersResponse{}, "list_token"),
),
)
// Update 2 users and see them in the refresh
r1 := allUsers[len(allUsers)-1]
r1.Description = wrapperspb.String("updated1")
resp1, err := a.UpdateUser(ctx, &pbs.UpdateUserRequest{
Id: r1.GetId(),
Item: &pb.User{Description: r1.GetDescription(), Version: r1.GetVersion()},
UpdateMask: &field_mask.FieldMask{Paths: []string{"description"}},
})
require.NoError(t, err)
r1.UpdatedTime = resp1.GetItem().GetUpdatedTime()
r1.Version = resp1.GetItem().GetVersion()
allUsers = append([]*pb.User{r1}, allUsers[:len(allUsers)-1]...)
r2 := allUsers[len(allUsers)-1]
r2.Description = wrapperspb.String("updated2")
resp2, err := a.UpdateUser(ctx, &pbs.UpdateUserRequest{
Id: r2.GetId(),
Item: &pb.User{Description: r2.GetDescription(), Version: r2.GetVersion()},
UpdateMask: &field_mask.FieldMask{Paths: []string{"description"}},
})
require.NoError(t, err)
r2.UpdatedTime = resp2.GetItem().GetUpdatedTime()
r2.Version = resp2.GetItem().GetVersion()
allUsers = append([]*pb.User{r2}, allUsers[:len(allUsers)-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.ListUsers(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.ListUsersResponse{
Items: []*pb.User{allUsers[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.ListUsersResponse{}, "list_token"),
),
)
// Get next page
req.ListToken = got.ListToken
got, err = a.ListUsers(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.ListUsersResponse{
Items: []*pb.User{allUsers[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.ListUsersResponse{}, "list_token"),
),
)
// Request new page with filter requiring looping
// to fill the page.
req.ListToken = ""
req.PageSize = 1
req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, allUsers[len(allUsers)-2].Id, allUsers[len(allUsers)-1].Id)
got, err = a.ListUsers(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListUsersResponse{
Items: []*pb.User{allUsers[len(allUsers)-2]},
ResponseType: "delta",
SortBy: "created_time",
SortDir: "desc",
// Should be empty again
RemovedIds: nil,
EstItemCount: itemCount,
},
protocmp.Transform(),
protocmp.SortRepeated(func(a, b string) bool {
return strings.Compare(a, b) < 0
}),
protocmp.IgnoreFields(&pbs.ListUsersResponse{}, "list_token"),
),
)
req.ListToken = got.ListToken
// Get the second page
got, err = a.ListUsers(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListUsersResponse{
Items: []*pb.User{allUsers[len(allUsers)-1]},
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.ListUsersResponse{}, "list_token"),
),
)
_, err = iamRepo.DeleteUser(ctx, safeToDeleteUser)
require.NoError(t, err)
req.ListToken = got.ListToken
got, err = a.ListUsers(ctx, req)
require.NoError(t, err)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListUsersResponse{
Items: nil,
ResponseType: "complete",
SortBy: "updated_time",
SortDir: "desc",
RemovedIds: []string{safeToDeleteUser},
EstItemCount: itemCount,
},
protocmp.Transform(),
protocmp.SortRepeated(func(a, b string) bool {
return strings.Compare(a, b) < 0
}),
protocmp.IgnoreFields(&pbs.ListUsersResponse{}, "list_token"),
),
)
}
func TestDelete(t *testing.T) {
u, _, repoFn := createDefaultUserAndRepo(t, false)
s, err := users.NewService(context.Background(), repoFn)
s, err := users.NewService(context.Background(), repoFn, 1000)
require.NoError(t, err, "Error when getting new user service.")
cases := []struct {
@@ -394,7 +779,7 @@ func TestDelete_twice(t *testing.T) {
assert, require := assert.New(t), require.New(t)
u, _, repoFn := createDefaultUserAndRepo(t, false)
s, err := users.NewService(context.Background(), repoFn)
s, err := users.NewService(context.Background(), repoFn, 1000)
require.NoError(err, "Error when getting new user service")
req := &pbs.DeleteUserRequest{
Id: u.GetPublicId(),
@@ -486,7 +871,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)
s, err := users.NewService(context.Background(), repoFn, 1000)
require.NoError(err, "Error when getting new user service.")
got, gErr := s.CreateUser(auth.DisabledAuthTestContext(repoFn, tc.req.GetItem().GetScopeId()), tc.req)
@@ -522,7 +907,7 @@ func TestCreate(t *testing.T) {
func TestUpdate(t *testing.T) {
u, _, repoFn := createDefaultUserAndRepo(t, false)
tested, err := users.NewService(context.Background(), repoFn)
tested, err := users.NewService(context.Background(), repoFn, 1000)
require.NoError(t, err, "Error when getting new user service.")
created := u.GetCreateTime().GetTimestamp().AsTime()
@@ -806,7 +1191,7 @@ func TestAddAccount(t *testing.T) {
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := users.NewService(ctx, repoFn)
s, err := users.NewService(ctx, repoFn, 1000)
require.NoError(t, err, "Error when getting new user service.")
o, _ := iam.TestScopes(t, iamRepo)
@@ -966,7 +1351,7 @@ func TestSetAccount(t *testing.T) {
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := users.NewService(ctx, repoFn)
s, err := users.NewService(ctx, repoFn, 1000)
require.NoError(t, err, "Error when getting new user service.")
o, _ := iam.TestScopes(t, iamRepo)
@@ -1128,7 +1513,7 @@ func TestRemoveAccount(t *testing.T) {
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := users.NewService(ctx, repoFn)
s, err := users.NewService(ctx, repoFn, 1000)
require.NoError(t, err, "Error when getting new user service.")
o, _ := iam.TestScopes(t, iamRepo)