1 parent
69a6f664de
commit
0fad770d8a
21 files changed
+2242
-339
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user