Paginate iam groups (#4194)

This commit is contained in:
Todd authored and Johan Brandhorst-Satzkorn committed 2024-01-05 07:43:21 -08:00
1 parent 8dc431fef5
commit 40d9c3fd1d
19 files changed
+2034 -270

No files matched your search

@@ -45,7 +45,7 @@ func TestListingScopeIds(t *testing.T) {
serversRepoFn := func() (*server.Repository, error) {
return server.NewRepository(ctx, rw, rw, kms)
}
s, err := groups.NewService(ctx, iamRepoFn)
s, err := groups.NewService(ctx, iamRepoFn, 1000)
require.NoError(t, err)
sessionsRepoFn := func(opt ...session.Option) (*session.Repository, error) {
+1 -1
View File
@@ -250,7 +250,7 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error {
services.RegisterTargetServiceServer(s, ts)
}
if _, ok := currentServices[services.GroupService_ServiceDesc.ServiceName]; !ok {
gs, err := groups.NewService(c.baseContext, c.IamRepoFn)
gs, err := groups.NewService(c.baseContext, c.IamRepoFn, c.conf.RawConfig.Controller.MaxPageSize)
if err != nil {
return fmt.Errorf("failed to create group handler service: %w", err)
}
@@ -16,12 +16,15 @@ 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"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/groups"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
"github.com/hashicorp/go-secure-stdlib/strutil"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/wrapperspb"
@@ -68,22 +71,27 @@ func init() {
type Service struct {
pbs.UnsafeGroupServiceServer
repoFn common.IamRepoFactory
repoFn common.IamRepoFactory
maxPageSize uint
}
var _ pbs.GroupServiceServer = (*Service)(nil)
// NewService returns a group service which handles group 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 = "groups.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
}
// ListGroups implements the interface pbs.GroupServiceServer.
func (s Service) ListGroups(ctx context.Context, req *pbs.ListGroupsRequest) (*pbs.ListGroupsResponse, error) {
const op = "groups.(Service).ListGroups"
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
@@ -111,50 +119,115 @@ func (s Service) ListGroups(ctx context.Context, req *pbs.ListGroupsRequest) (*p
return &pbs.ListGroupsResponse{}, nil
}
gl, err := s.listFromRepo(ctx, scopeIds)
if err != nil {
return nil, err
}
if len(gl) == 0 {
return &pbs.ListGroupsResponse{}, 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.Group, 0, len(gl))
res := perms.Resource{
Type: resource.Group,
}
for _, item := range gl {
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.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))
}
item, err := toProto(ctx, item, nil, outputOpts...)
var filterItemFn func(ctx context.Context, item *iam.Group) (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.Group) (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.Group) (bool, error) {
return true, nil
}
}
return &pbs.ListGroupsResponse{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.Group]
var sortBy string
if req.GetListToken() == "" {
sortBy = "created_time"
listResp, err = iam.ListGroups(ctx, grantsHash, pageSize, filterItemFn, repo, scopeIds)
if err != nil {
return nil, err
}
} else {
listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.Group, grantsHash)
if err != nil {
return nil, err
}
switch st := listToken.Subtype.(type) {
case *listtoken.PaginationToken:
sortBy = "created_time"
listResp, err = iam.ListGroupsPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds)
if err != nil {
return nil, err
}
case *listtoken.StartRefreshToken:
sortBy = "updated_time"
listResp, err = iam.ListGroupsRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds)
if err != nil {
return nil, err
}
case *listtoken.RefreshToken:
sortBy = "updated_time"
listResp, err = iam.ListGroupsRefreshPage(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.Group, 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.ListGroupsResponse{
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_GROUP)
if err != nil {
return nil, err
}
}
return resp, nil
}
// GetGroups implements the interface pbs.GroupServiceServer.
@@ -493,19 +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.Group, error) {
const op = "groups.(Service).listFromRepo"
repo, err := s.repoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
gl, err := repo.ListGroups(ctx, scopeIds, iam.WithLimit(-1))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return gl, nil
}
func (s Service) addMembersInRepo(ctx context.Context, groupId string, userIds []string, version uint32) (*iam.Group, []*iam.GroupMember, error) {
const op = "groups.(Service).addMembersInRepo"
repo, err := s.repoFn()
@@ -777,3 +837,26 @@ func validateRemoveGroupMembersRequest(req *pbs.RemoveGroupMembersRequest) error
}
return nil
}
func newOutputOpts(ctx context.Context, item *iam.Group, scopeInfoMap map[string]*scopes.ScopeInfo, authResults auth.VerifyResults) ([]handlers.Option, bool) {
res := perms.Resource{
Type: resource.Group,
}
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,18 +7,25 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"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/groups"
"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"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/groups"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
@@ -201,7 +208,7 @@ func TestGet(t *testing.T) {
req := proto.Clone(toMerge).(*pbs.GetGroupRequest)
proto.Merge(req, tc.req)
s, err := groups.NewService(ctx, repoFn)
s, err := groups.NewService(ctx, repoFn, 1000)
require.NoError(err, "Couldn't create new group service.")
got, gErr := s.GetGroup(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req)
@@ -231,10 +238,22 @@ func TestList(t *testing.T) {
}
oNoGroups, pWithGroups := iam.TestScopes(t, iamRepo)
oWithGroups, pNoGroups := iam.TestScopes(t, iamRepo)
var wantGlobalGroups []*pb.Group
var wantOrgGroups []*pb.Group
var wantProjGroups []*pb.Group
var totalGroups []*pb.Group
for i := 0; i < 10; i++ {
gg := iam.TestGroup(t, conn, "global")
wantGlobalGroups = append(wantGlobalGroups, &pb.Group{
Id: gg.GetPublicId(),
ScopeId: gg.GetScopeId(),
Scope: &scopes.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: "global", Description: "Global Scope"},
CreatedTime: gg.GetCreateTime().GetTimestamp(),
UpdatedTime: gg.GetUpdateTime().GetTimestamp(),
Version: 1,
AuthorizedActions: testAuthorizedActions,
})
totalGroups = append(totalGroups, wantGlobalGroups[i])
og := iam.TestGroup(t, conn, oWithGroups.GetPublicId())
wantOrgGroups = append(wantOrgGroups, &pb.Group{
Id: og.GetPublicId(),
@@ -259,6 +278,11 @@ func TestList(t *testing.T) {
totalGroups = append(totalGroups, wantProjGroups[i])
}
slices.Reverse(wantProjGroups)
slices.Reverse(wantOrgGroups)
slices.Reverse(wantGlobalGroups)
slices.Reverse(totalGroups)
cases := []struct {
name string
req *pbs.ListGroupsRequest
@@ -266,60 +290,119 @@ func TestList(t *testing.T) {
err error
}{
{
name: "List Many Group",
name: "List Global Groups",
req: &pbs.ListGroupsRequest{ScopeId: "global"},
res: &pbs.ListGroupsResponse{
Items: wantGlobalGroups,
EstItemCount: uint32(len(wantGlobalGroups)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List Org Group",
req: &pbs.ListGroupsRequest{ScopeId: oWithGroups.GetPublicId()},
res: &pbs.ListGroupsResponse{Items: wantOrgGroups},
res: &pbs.ListGroupsResponse{
Items: wantOrgGroups,
EstItemCount: uint32(len(wantOrgGroups)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List No Groups",
req: &pbs.ListGroupsRequest{ScopeId: oNoGroups.GetPublicId()},
res: &pbs.ListGroupsResponse{},
res: &pbs.ListGroupsResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List Many Project Group",
req: &pbs.ListGroupsRequest{ScopeId: pWithGroups.GetPublicId()},
res: &pbs.ListGroupsResponse{Items: wantProjGroups},
res: &pbs.ListGroupsResponse{
Items: wantProjGroups,
EstItemCount: uint32(len(wantProjGroups)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List No Project Groups",
req: &pbs.ListGroupsRequest{ScopeId: pNoGroups.GetPublicId()},
res: &pbs.ListGroupsResponse{},
res: &pbs.ListGroupsResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List proj groups recursively",
req: &pbs.ListGroupsRequest{ScopeId: pWithGroups.GetPublicId(), Recursive: true},
res: &pbs.ListGroupsResponse{
Items: wantProjGroups,
Items: wantProjGroups,
EstItemCount: uint32(len(wantProjGroups)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List org groups recursively",
req: &pbs.ListGroupsRequest{ScopeId: oNoGroups.GetPublicId(), Recursive: true},
res: &pbs.ListGroupsResponse{
Items: wantProjGroups,
Items: wantProjGroups,
EstItemCount: uint32(len(wantProjGroups)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List global groups recursively",
req: &pbs.ListGroupsRequest{ScopeId: "global", Recursive: true},
res: &pbs.ListGroupsResponse{
Items: totalGroups,
Items: totalGroups,
EstItemCount: uint32(len(totalGroups)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter to org groups",
req: &pbs.ListGroupsRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithGroups.GetPublicId())},
res: &pbs.ListGroupsResponse{Items: wantOrgGroups},
res: &pbs.ListGroupsResponse{
Items: wantOrgGroups,
EstItemCount: uint32(len(wantOrgGroups)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter to project groups",
req: &pbs.ListGroupsRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, pWithGroups.GetPublicId())},
res: &pbs.ListGroupsResponse{Items: wantProjGroups},
res: &pbs.ListGroupsResponse{
Items: wantProjGroups,
EstItemCount: uint32(len(wantProjGroups)),
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter to no groups",
req: &pbs.ListGroupsRequest{ScopeId: "global", Recursive: true, Filter: `"/item/id"=="doesntmatch"`},
res: &pbs.ListGroupsResponse{},
res: &pbs.ListGroupsResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter Bad Format",
@@ -330,7 +413,7 @@ func TestList(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := groups.NewService(ctx, repoFn)
s, err := groups.NewService(ctx, repoFn, 1000)
require.NoError(err, "Couldn't create new group service.")
// Test with a non-anon user
@@ -345,6 +428,7 @@ func TestList(t *testing.T) {
got,
tc.res,
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListGroupsResponse{}, "list_token"),
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
@@ -364,10 +448,365 @@ func TestList(t *testing.T) {
}
}
func groupToProto(u *iam.Group, si *scopes.ScopeInfo, authorizedActions []string) *pb.Group {
pu := &pb.Group{
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 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)
oNoGroups, pWithGroups := iam.TestScopes(t, iamRepo)
oWithGroups, pNoGroups := iam.TestScopes(t, iamRepo)
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 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{oWithGroups, oNoGroups, pWithGroups, pNoGroups} {
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 allGroups []*pb.Group
var projGroups []*pb.Group
var orgGroups []*pb.Group
var globalGroups []*pb.Group
var safeToDeleteGroup string
globalScopeInfo := &scopes.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: "global", Description: "Global Scope"}
orgScopeInfo := &scopes.ScopeInfo{Id: oWithGroups.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}
projScopeInfo := &scopes.ScopeInfo{Id: pWithGroups.GetPublicId(), Type: scope.Project.String(), ParentScopeId: pWithGroups.GetParentId()}
for i := 0; i < 10; i++ {
gg := iam.TestGroup(t, conn, scope.Global.String())
allGroups = append(allGroups, groupToProto(gg, globalScopeInfo, testAuthorizedActions))
globalGroups = append(globalGroups, groupToProto(gg, globalScopeInfo, testAuthorizedActions))
og := iam.TestGroup(t, conn, oWithGroups.GetPublicId())
allGroups = append(allGroups, groupToProto(og, orgScopeInfo, testAuthorizedActions))
orgGroups = append(orgGroups, groupToProto(og, orgScopeInfo, testAuthorizedActions))
pg := iam.TestGroup(t, conn, pWithGroups.GetPublicId())
allGroups = append(allGroups, groupToProto(pg, projScopeInfo, testAuthorizedActions))
projGroups = append(projGroups, groupToProto(pg, projScopeInfo, testAuthorizedActions))
safeToDeleteGroup = pg.GetPublicId()
}
slices.Reverse(allGroups)
slices.Reverse(projGroups)
slices.Reverse(orgGroups)
slices.Reverse(globalGroups)
a, err := groups.NewService(ctx, iamRepoFn, 1000)
require.NoError(t, err, "Couldn't create new group service.")
// Run analyze to update postgres estimates
_, err = sqlDB.ExecContext(context.Background(), "analyze")
require.NoError(t, err)
itemCount := uint32(len(allGroups))
testPageSize := int((itemCount - 2) / 2)
// Start paginating, recursively
req := &pbs.ListGroupsRequest{
ScopeId: "global",
Recursive: true,
Filter: "",
ListToken: "",
PageSize: uint32(testPageSize),
}
got, err := a.ListGroups(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.ListGroupsResponse{
Items: allGroups[0:testPageSize],
ResponseType: "delta",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
// In addition to the added groups, there are the groups 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.ListGroupsResponse{}, "list_token"),
),
)
// Request second page
req.ListToken = got.ListToken
got, err = a.ListGroups(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.ListGroupsResponse{
Items: allGroups[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.ListGroupsResponse{}, "list_token"),
),
)
// Request rest of results
req.ListToken = got.ListToken
req.PageSize = 10
got, err = a.ListGroups(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.ListGroupsResponse{
Items: allGroups[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.ListGroupsResponse{}, "list_token"),
),
)
// Update 2 groups and see them in the refresh
g1 := allGroups[len(allGroups)-1]
g1.Description = wrapperspb.String("updated1")
resp1, err := a.UpdateGroup(ctx, &pbs.UpdateGroupRequest{
Id: g1.GetId(),
Item: &pb.Group{Description: g1.GetDescription(), Version: g1.GetVersion()},
UpdateMask: &field_mask.FieldMask{Paths: []string{"description"}},
})
require.NoError(t, err)
g1.UpdatedTime = resp1.GetItem().GetUpdatedTime()
g1.Version = resp1.GetItem().GetVersion()
allGroups = append([]*pb.Group{g1}, allGroups[:len(allGroups)-1]...)
g2 := allGroups[len(allGroups)-1]
g2.Description = wrapperspb.String("updated2")
resp2, err := a.UpdateGroup(ctx, &pbs.UpdateGroupRequest{
Id: g2.GetId(),
Item: &pb.Group{Description: g2.GetDescription(), Version: g2.GetVersion()},
UpdateMask: &field_mask.FieldMask{Paths: []string{"description"}},
})
require.NoError(t, err)
g2.UpdatedTime = resp2.GetItem().GetUpdatedTime()
g2.Version = resp2.GetItem().GetVersion()
allGroups = append([]*pb.Group{g2}, allGroups[:len(allGroups)-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.ListGroups(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.ListGroupsResponse{
Items: []*pb.Group{allGroups[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.ListGroupsResponse{}, "list_token"),
),
)
// Get next page
req.ListToken = got.ListToken
got, err = a.ListGroups(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.ListGroupsResponse{
Items: []*pb.Group{allGroups[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.ListGroupsResponse{}, "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`, allGroups[len(allGroups)-2].Id, allGroups[len(allGroups)-1].Id)
got, err = a.ListGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListGroupsResponse{
Items: []*pb.Group{allGroups[len(allGroups)-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.ListGroupsResponse{}, "list_token"),
),
)
req.ListToken = got.ListToken
// Get the second page
got, err = a.ListGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListGroupsResponse{
Items: []*pb.Group{allGroups[len(allGroups)-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.ListGroupsResponse{}, "list_token"),
),
)
_, err = iamRepo.DeleteGroup(ctx, safeToDeleteGroup)
require.NoError(t, err)
req.ListToken = got.ListToken
got, err = a.ListGroups(ctx, req)
require.NoError(t, err)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListGroupsResponse{
Items: nil,
ResponseType: "complete",
SortBy: "updated_time",
SortDir: "desc",
RemovedIds: []string{safeToDeleteGroup},
EstItemCount: itemCount,
},
protocmp.Transform(),
protocmp.SortRepeated(func(a, b string) bool {
return strings.Compare(a, b) < 0
}),
protocmp.IgnoreFields(&pbs.ListGroupsResponse{}, "list_token"),
),
)
}
func TestDelete(t *testing.T) {
og, pg, repoFn := createDefaultGroupsAndRepo(t)
s, err := groups.NewService(context.Background(), repoFn)
s, err := groups.NewService(context.Background(), repoFn, 1000)
require.NoError(t, err, "Error when getting new group service.")
cases := []struct {
@@ -433,7 +872,7 @@ func TestDelete_twice(t *testing.T) {
assert, require := assert.New(t), require.New(t)
og, pg, repoFn := createDefaultGroupsAndRepo(t)
s, err := groups.NewService(context.Background(), repoFn)
s, err := groups.NewService(context.Background(), repoFn, 1000)
require.NoError(err, "Error when getting new group service")
scopeId := og.GetScopeId()
req := &pbs.DeleteGroupRequest{
@@ -559,7 +998,7 @@ func TestCreate(t *testing.T) {
req := proto.Clone(toMerge).(*pbs.CreateGroupRequest)
proto.Merge(req, tc.req)
s, err := groups.NewService(context.Background(), repoFn)
s, err := groups.NewService(context.Background(), repoFn, 1000)
require.NoError(err, "Error when getting new group service.")
got, gErr := s.CreateGroup(auth.DisabledAuthTestContext(repoFn, req.GetItem().GetScopeId()), req)
@@ -635,7 +1074,7 @@ func TestUpdate(t *testing.T) {
Id: og.GetPublicId(),
}
tested, err := groups.NewService(ctx, repoFn)
tested, err := groups.NewService(ctx, repoFn, 1000)
require.NoError(t, err, "Error creating new service")
cases := []struct {
name string
@@ -1014,7 +1453,7 @@ func TestAddMember(t *testing.T) {
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := groups.NewService(context.Background(), repoFn)
s, err := groups.NewService(context.Background(), repoFn, 100)
require.NoError(t, err, "Error when getting new group service.")
o, p := iam.TestScopes(t, iamRepo)
@@ -1149,7 +1588,7 @@ func TestSetMember(t *testing.T) {
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := groups.NewService(context.Background(), repoFn)
s, err := groups.NewService(context.Background(), repoFn, 1000)
require.NoError(t, err, "Error when getting new group service.")
o, p := iam.TestScopes(t, iamRepo)
@@ -1279,7 +1718,7 @@ func TestRemoveMember(t *testing.T) {
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := groups.NewService(context.Background(), repoFn)
s, err := groups.NewService(context.Background(), repoFn, 1000)
require.NoError(t, err, "Error when getting new grp service.")
o, p := iam.TestScopes(t, iamRepo)