// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package groups_test 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" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var testAuthorizedActions = []string{"no-op", "read", "update", "delete", "add-members", "set-members", "remove-members"} // Creates an org scoped group and a project scoped group. func createDefaultGroupsAndRepo(t *testing.T) (*iam.Group, *iam.Group, func() (*iam.Repository, error)) { t.Helper() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } o, p := iam.TestScopes(t, iamRepo) og := iam.TestGroup(t, conn, o.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) pg := iam.TestGroup(t, conn, p.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) return og, pg, repoFn } func equalMembers(g *pb.Group, members []string) bool { if len(g.Members) != len(members) { return false } for _, m := range members { var foundInMembers bool var foundInPrincipalIds bool for _, v := range g.Members { if v.Id == m { foundInMembers = true } } for _, v := range g.MemberIds { if v == m { foundInPrincipalIds = true } } if !foundInMembers || !foundInPrincipalIds { return false } } return true } func TestGet(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } o, p := iam.TestScopes(t, iamRepo) u := iam.TestUser(t, iamRepo, o.GetPublicId()) og := iam.TestGroup(t, conn, o.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) _ = iam.TestGroupMember(t, conn, og.GetPublicId(), u.GetPublicId()) pg := iam.TestGroup(t, conn, p.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) _ = iam.TestGroupMember(t, conn, pg.GetPublicId(), u.GetPublicId()) toMerge := &pbs.GetGroupRequest{ Id: og.GetPublicId(), } wantOrgGroup := &pb.Group{ Id: og.GetPublicId(), ScopeId: og.GetScopeId(), Scope: &scopes.ScopeInfo{Id: og.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: og.GetName()}, Description: &wrapperspb.StringValue{Value: og.GetDescription()}, CreatedTime: og.CreateTime.GetTimestamp(), UpdatedTime: og.UpdateTime.GetTimestamp(), Version: 1, MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, } wantProjGroup := &pb.Group{ Id: pg.GetPublicId(), ScopeId: pg.GetScopeId(), Scope: &scopes.ScopeInfo{Id: pg.GetScopeId(), Type: scope.Project.String(), ParentScopeId: o.GetPublicId()}, Name: &wrapperspb.StringValue{Value: pg.GetName()}, Description: &wrapperspb.StringValue{Value: pg.GetDescription()}, CreatedTime: pg.CreateTime.GetTimestamp(), UpdatedTime: pg.UpdateTime.GetTimestamp(), Version: 1, MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, } cases := []struct { name string scopeId string req *pbs.GetGroupRequest res *pbs.GetGroupResponse err error }{ { name: "Get an Existing Group", scopeId: og.GetScopeId(), req: &pbs.GetGroupRequest{Id: og.GetPublicId()}, res: &pbs.GetGroupResponse{Item: wantOrgGroup}, }, { name: "Get a non existent Group", req: &pbs.GetGroupRequest{Id: globals.GroupPrefix + "_DoesntExis"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Wrong id prefix", req: &pbs.GetGroupRequest{Id: "j_1234567890"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "space in id", req: &pbs.GetGroupRequest{Id: globals.GroupPrefix + "_1 23456789"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Project Scoped Get an Existing Group", scopeId: pg.GetScopeId(), req: &pbs.GetGroupRequest{Id: pg.GetPublicId()}, res: &pbs.GetGroupResponse{Item: wantProjGroup}, }, { name: "Project Scoped Get a non existent Group", scopeId: pg.GetScopeId(), req: &pbs.GetGroupRequest{Id: globals.GroupPrefix + "_DoesntExis"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Project Scoped Wrong id prefix", scopeId: pg.GetScopeId(), req: &pbs.GetGroupRequest{Id: "j_1234567890"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Project Scoped space in id", scopeId: pg.GetScopeId(), req: &pbs.GetGroupRequest{Id: globals.GroupPrefix + "_1 23456789"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) req := proto.Clone(toMerge).(*pbs.GetGroupRequest) proto.Merge(req, tc.req) 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) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetGroup(%+v) got error %v, wanted %v", req, gErr, tc.err) } assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "GetGroup(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } func TestList(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } 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(), ScopeId: og.GetScopeId(), Scope: &scopes.ScopeInfo{Id: oWithGroups.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, CreatedTime: og.GetCreateTime().GetTimestamp(), UpdatedTime: og.GetUpdateTime().GetTimestamp(), Version: 1, AuthorizedActions: testAuthorizedActions, }) totalGroups = append(totalGroups, wantOrgGroups[i]) pg := iam.TestGroup(t, conn, pWithGroups.GetPublicId()) wantProjGroups = append(wantProjGroups, &pb.Group{ Id: pg.GetPublicId(), ScopeId: pg.GetScopeId(), Scope: &scopes.ScopeInfo{Id: pWithGroups.GetPublicId(), Type: scope.Project.String(), ParentScopeId: oNoGroups.GetPublicId()}, CreatedTime: pg.GetCreateTime().GetTimestamp(), UpdatedTime: pg.GetUpdateTime().GetTimestamp(), Version: 1, AuthorizedActions: testAuthorizedActions, }) totalGroups = append(totalGroups, wantProjGroups[i]) } slices.Reverse(wantProjGroups) slices.Reverse(wantOrgGroups) slices.Reverse(wantGlobalGroups) slices.Reverse(totalGroups) cases := []struct { name string req *pbs.ListGroupsRequest res *pbs.ListGroupsResponse err error }{ { 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, EstItemCount: uint32(len(wantOrgGroups)), ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List No Groups", req: &pbs.ListGroupsRequest{ScopeId: oNoGroups.GetPublicId()}, 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, 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{ 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, 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, 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, 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, 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, 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{ ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Filter Bad Format", req: &pbs.ListGroupsRequest{ScopeId: "global", Filter: `"//id/"=="bad"`}, err: handlers.InvalidArgumentErrorf("bad format", nil), }, } 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, 1000) require.NoError(err, "Couldn't create new group service.") // Test with a non-anon user got, gErr := s.ListGroups(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListGroups(%q) got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) return } require.NoError(gErr) assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListGroupsResponse{}, "list_token"), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "ListGroups(%q) got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Test the anon case got, gErr = s.ListGroups(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) require.NoError(gErr) assert.Len(got.Items, len(tc.res.Items)) for _, item := range got.GetItems() { require.Nil(item.CreatedTime) require.Nil(item.Members) require.Nil(item.UpdatedTime) require.Zero(item.Version) } }) } } 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(), "ids=*;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(), "ids=*;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"), ), ) // Create unauthenticated user unauthAt := authtoken.TestAuthToken(t, conn, kms, oWithGroups.GetPublicId()) unauthR := iam.TestRole(t, conn, pWithGroups.GetPublicId()) _ = iam.TestUserRole(t, conn, unauthR.GetPublicId(), unauthAt.GetIamUserId()) // Make a request with the unauthenticated user, // ensure the response is 403 forbidden. 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.ListGroups(ctx, &pbs.ListGroupsRequest{ ScopeId: "global", Recursive: true, }) require.Error(t, err) assert.Equal(t, handlers.ForbiddenError(), err) } func TestDelete(t *testing.T) { og, pg, repoFn := createDefaultGroupsAndRepo(t) s, err := groups.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new group service.") cases := []struct { name string scopeId string req *pbs.DeleteGroupRequest res *pbs.DeleteGroupResponse err error }{ { name: "Delete an Existing Group", scopeId: og.GetScopeId(), req: &pbs.DeleteGroupRequest{ Id: og.GetPublicId(), }, }, { name: "Delete bad group id", scopeId: og.GetScopeId(), req: &pbs.DeleteGroupRequest{ Id: globals.GroupPrefix + "_doesntexis", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Bad Group Id formatting", scopeId: og.GetScopeId(), req: &pbs.DeleteGroupRequest{ Id: "bad_format", }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Project Scoped Delete an Existing Group", scopeId: pg.GetScopeId(), req: &pbs.DeleteGroupRequest{ Id: pg.GetPublicId(), }, }, { name: "Project Scoped Delete bad group id", scopeId: pg.GetScopeId(), req: &pbs.DeleteGroupRequest{ Id: globals.GroupPrefix + "_doesntexis", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) got, gErr := s.DeleteGroup(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteGroup(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } assert.EqualValuesf(tc.res, got, "DeleteGroup(%+v) got response %q, wanted %q", tc.req, got, tc.res) }) } } 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, 1000) require.NoError(err, "Error when getting new group service") scopeId := og.GetScopeId() req := &pbs.DeleteGroupRequest{ Id: og.GetPublicId(), } ctx := auth.DisabledAuthTestContext(repoFn, scopeId) _, gErr := s.DeleteGroup(ctx, req) assert.NoError(gErr, "First attempt") _, gErr = s.DeleteGroup(ctx, req) assert.Error(gErr, "Second attempt") assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected permission denied for the second delete.") scopeId = pg.GetScopeId() projReq := &pbs.DeleteGroupRequest{ Id: pg.GetPublicId(), } ctx = auth.DisabledAuthTestContext(repoFn, scopeId) _, gErr = s.DeleteGroup(ctx, projReq) assert.NoError(gErr, "First attempt") _, gErr = s.DeleteGroup(ctx, projReq) assert.Error(gErr, "Second attempt") assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected permission denied for the second delete.") } func TestCreate(t *testing.T) { defaultOGroup, defaultPGroup, repoFn := createDefaultGroupsAndRepo(t) defaultCreated := defaultOGroup.GetCreateTime().GetTimestamp().AsTime() toMerge := &pbs.CreateGroupRequest{} cases := []struct { name string req *pbs.CreateGroupRequest res *pbs.CreateGroupResponse err error }{ { name: "Create a valid Group", req: &pbs.CreateGroupRequest{Item: &pb.Group{ ScopeId: defaultOGroup.GetScopeId(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, }}, res: &pbs.CreateGroupResponse{ Uri: fmt.Sprintf("groups/%s_", globals.GroupPrefix), Item: &pb.Group{ ScopeId: defaultOGroup.GetScopeId(), Scope: &scopes.ScopeInfo{Id: defaultOGroup.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Version: 1, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Create a global Group", req: &pbs.CreateGroupRequest{Item: &pb.Group{ ScopeId: scope.Global.String(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, }}, res: &pbs.CreateGroupResponse{ Uri: fmt.Sprintf("groups/%s_", globals.GroupPrefix), Item: &pb.Group{ ScopeId: scope.Global.String(), Scope: &scopes.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Version: 1, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Create a valid Project Scoped Group", req: &pbs.CreateGroupRequest{ Item: &pb.Group{ ScopeId: defaultPGroup.GetScopeId(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.CreateGroupResponse{ Uri: fmt.Sprintf("groups/%s_", globals.GroupPrefix), Item: &pb.Group{ ScopeId: defaultPGroup.GetScopeId(), Scope: &scopes.ScopeInfo{Id: defaultPGroup.GetScopeId(), Type: scope.Project.String(), ParentScopeId: defaultOGroup.GetScopeId()}, Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Version: 1, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Can't specify Id", req: &pbs.CreateGroupRequest{Item: &pb.Group{ Id: globals.GroupPrefix + "_notallowed", }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Created Time", req: &pbs.CreateGroupRequest{Item: &pb.Group{ CreatedTime: timestamppb.Now(), }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Update Time", req: &pbs.CreateGroupRequest{Item: &pb.Group{ UpdatedTime: timestamppb.Now(), }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) req := proto.Clone(toMerge).(*pbs.CreateGroupRequest) proto.Merge(req, tc.req) 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) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "CreateGroup(%+v) got error %v, wanted %v", req, gErr, tc.err) } if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) assert.True(strings.HasPrefix(got.GetItem().GetId(), globals.GroupPrefix+"_")) gotCreateTime := got.GetItem().GetCreatedTime().AsTime() gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() // Verify it is a group created after the test setup's default group assert.True(gotCreateTime.After(defaultCreated), "New group should have been created after default group. Was created %v, which is after %v", gotCreateTime, defaultCreated) assert.True(gotUpdateTime.After(defaultCreated), "New group should have been updated after default group. Was updated %v, which is after %v", gotUpdateTime, defaultCreated) // Clear all values which are hard to compare against. got.Uri, tc.res.Uri = "", "" got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "CreateGroup(%q) got response %q, wanted %q", req, got, tc.res) }) } } func TestUpdate(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } o, p := iam.TestScopes(t, iamRepo) u := iam.TestUser(t, iamRepo, o.GetPublicId()) og := iam.TestGroup(t, conn, o.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) _ = iam.TestGroupMember(t, conn, og.GetPublicId(), u.GetPublicId()) pg := iam.TestGroup(t, conn, p.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) _ = iam.TestGroupMember(t, conn, pg.GetPublicId(), u.GetPublicId()) var ogVersion uint32 = 1 var pgVersion uint32 = 1 resetGroups := func(proj bool) { repo, err := repoFn() require.NoError(t, err, "Couldn't get a new repo") if proj { pgVersion++ pg, _, _, err = repo.UpdateGroup(ctx, pg, pgVersion, []string{"Name", "Description"}) require.NoError(t, err, "Failed to reset the group") pgVersion++ } else { ogVersion++ og, _, _, err = repo.UpdateGroup(ctx, og, ogVersion, []string{"Name", "Description"}) require.NoError(t, err, "Failed to reset the group") ogVersion++ } } created := og.GetCreateTime().GetTimestamp().AsTime() toMerge := &pbs.UpdateGroupRequest{ Id: og.GetPublicId(), } tested, err := groups.NewService(ctx, repoFn, 1000) require.NoError(t, err, "Error creating new service") cases := []struct { name string scopeId string req *pbs.UpdateGroupRequest res *pbs.UpdateGroupResponse err error }{ { name: "Update an Existing Group", scopeId: og.GetScopeId(), req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateGroupResponse{ Item: &pb.Group{ Id: og.GetPublicId(), ScopeId: og.GetScopeId(), Scope: &scopes.ScopeInfo{Id: og.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: og.GetCreateTime().GetTimestamp(), MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Multiple Paths in single string", scopeId: og.GetScopeId(), req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name,description"}, }, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateGroupResponse{ Item: &pb.Group{ Id: og.GetPublicId(), ScopeId: og.GetScopeId(), Scope: &scopes.ScopeInfo{Id: og.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: og.GetCreateTime().GetTimestamp(), MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Update an Existing Project Scoped Group", scopeId: pg.GetScopeId(), req: &pbs.UpdateGroupRequest{ Id: pg.GetPublicId(), UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateGroupResponse{ Item: &pb.Group{ Id: pg.GetPublicId(), ScopeId: pg.GetScopeId(), Scope: &scopes.ScopeInfo{Id: pg.GetScopeId(), Type: scope.Project.String(), ParentScopeId: og.GetScopeId()}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: pg.GetCreateTime().GetTimestamp(), MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Multiple Paths in single string", scopeId: pg.GetScopeId(), req: &pbs.UpdateGroupRequest{ Id: pg.GetPublicId(), UpdateMask: &field_mask.FieldMask{ Paths: []string{"name,description"}, }, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateGroupResponse{ Item: &pb.Group{ Id: pg.GetPublicId(), ScopeId: pg.GetScopeId(), Scope: &scopes.ScopeInfo{Id: pg.GetScopeId(), Type: scope.Project.String(), ParentScopeId: og.GetScopeId()}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: pg.GetCreateTime().GetTimestamp(), MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "No Update Mask", req: &pbs.UpdateGroupRequest{ Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "No Paths in Mask", scopeId: og.GetScopeId(), req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Only non-existent paths in Mask", scopeId: og.GetScopeId(), req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistent_field"}}, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unset Name", scopeId: og.GetScopeId(), req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name"}, }, Item: &pb.Group{ Description: &wrapperspb.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateGroupResponse{ Item: &pb.Group{ Id: og.GetPublicId(), ScopeId: og.GetScopeId(), Scope: &scopes.ScopeInfo{Id: og.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Description: &wrapperspb.StringValue{Value: "default"}, CreatedTime: og.GetCreateTime().GetTimestamp(), MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Update Only Name", scopeId: og.GetScopeId(), req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name"}, }, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateGroupResponse{ Item: &pb.Group{ Id: og.GetPublicId(), ScopeId: og.GetScopeId(), Scope: &scopes.ScopeInfo{Id: og.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "default"}, CreatedTime: og.GetCreateTime().GetTimestamp(), MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Update Only Description", scopeId: og.GetScopeId(), req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "ignored"}, Description: &wrapperspb.StringValue{Value: "notignored"}, }, }, res: &pbs.UpdateGroupResponse{ Item: &pb.Group{ Id: og.GetPublicId(), ScopeId: og.GetScopeId(), Scope: &scopes.ScopeInfo{Id: og.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "default"}, Description: &wrapperspb.StringValue{Value: "notignored"}, CreatedTime: og.GetCreateTime().GetTimestamp(), MemberIds: []string{u.GetPublicId()}, Members: []*pb.Member{ { Id: u.GetPublicId(), ScopeId: u.GetScopeId(), }, }, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Update a Non Existing Group", req: &pbs.UpdateGroupRequest{ Id: globals.GroupPrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.Group{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Cant change Id", req: &pbs.UpdateGroupRequest{ Id: og.GetPublicId(), UpdateMask: &field_mask.FieldMask{ Paths: []string{"id"}, }, Item: &pb.Group{ Id: globals.GroupPrefix + "_somethinge", Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "new desc"}, }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Created Time", req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"created_time"}, }, Item: &pb.Group{ CreatedTime: timestamppb.Now(), }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Updated Time", req: &pbs.UpdateGroupRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"updated_time"}, }, Item: &pb.Group{ UpdatedTime: timestamppb.Now(), }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ver := ogVersion if tc.req.Id == pg.PublicId { ver = pgVersion } tc.req.Item.Version = ver assert, require := assert.New(t), require.New(t) req := proto.Clone(toMerge).(*pbs.UpdateGroupRequest) proto.Merge(req, tc.req) // Test with bad version (too high, too low) req.Item.Version = ver + 2 _, gErr := tested.UpdateGroup(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) require.Error(gErr) req.Item.Version = ver - 1 _, gErr = tested.UpdateGroup(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) require.Error(gErr) req.Item.Version = ver got, gErr := tested.UpdateGroup(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "UpdateGroup(%+v) got error %v, wanted %v", req, gErr, tc.err) } if tc.err == nil { defer resetGroups(req.Id == pg.PublicId) } if got != nil { assert.NotNilf(tc.res, "Expected UpdateGroup response to be nil, but was %v", got) gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() // Verify it is a group updated after it was created assert.True(gotUpdateTime.After(created), "Updated group should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) // Clear all values which are hard to compare against. got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil assert.Equal(ver+1, got.GetItem().GetVersion()) tc.res.Item.Version = ver + 1 } assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "UpdateGroup(%q) got response %q, wanted %q", req, got, tc.res) }) } } func TestAddMember(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := groups.NewService(context.Background(), repoFn, 100) require.NoError(t, err, "Error when getting new group service.") o, p := iam.TestScopes(t, iamRepo) users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), } addCases := []struct { name string setup func(*iam.Group) addUsers []string addGroups []string resultUsers []string resultGroups []string wantErr bool }{ { name: "Add user on empty group", setup: func(g *iam.Group) {}, addUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Add user on populated group", setup: func(g *iam.Group) { iam.TestGroupMember(t, conn, g.GetPublicId(), users[0].GetPublicId()) }, addUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[0].GetPublicId(), users[1].GetPublicId()}, }, { name: "Add duplicate user on populated group", setup: func(g *iam.Group) { iam.TestGroupMember(t, conn, g.GetPublicId(), users[0].GetPublicId()) }, addUsers: []string{users[1].GetPublicId(), users[1].GetPublicId()}, resultUsers: []string{users[0].GetPublicId(), users[1].GetPublicId()}, }, { name: "Add empty on populated group", setup: func(g *iam.Group) { iam.TestGroupMember(t, conn, g.GetPublicId(), users[0].GetPublicId()) iam.TestGroupMember(t, conn, g.GetPublicId(), users[1].GetPublicId()) }, wantErr: true, }, { name: "Add invalid u_recovery to group", setup: func(g *iam.Group) {}, addUsers: []string{globals.RecoveryUserId}, wantErr: true, }, } for _, tc := range addCases { for _, scp := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scp.GetType(), func(t *testing.T) { grp := iam.TestGroup(t, conn, scp.GetPublicId()) tc.setup(grp) req := &pbs.AddGroupMembersRequest{ Id: grp.GetPublicId(), Version: grp.GetVersion(), MemberIds: tc.addUsers, } got, err := s.AddGroupMembers(auth.DisabledAuthTestContext(repoFn, scp.GetPublicId()), req) if tc.wantErr { assert.Error(t, err) return } s, ok := status.FromError(err) require.True(t, ok) require.NoError(t, err, "Got error: %v", s) assert.True(t, equalMembers(got.GetItem(), tc.resultUsers)) }) } } grp := iam.TestGroup(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.AddGroupMembersRequest err error }{ { name: "Bad Group Id", req: &pbs.AddGroupMembersRequest{ Id: "bad id", Version: grp.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalid user id in member list", req: &pbs.AddGroupMembersRequest{ Id: grp.GetPublicId(), Version: grp.GetVersion(), MemberIds: []string{"invalid"}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "u_recovery", req: &pbs.AddGroupMembersRequest{ Id: grp.GetPublicId(), Version: grp.GetVersion(), MemberIds: []string{globals.RecoveryUserId}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.AddGroupMembers(auth.DisabledAuthTestContext(repoFn, grp.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "AddGroupMembers(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } } func TestSetMember(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := groups.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new group service.") o, p := iam.TestScopes(t, iamRepo) users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), } setCases := []struct { name string setup func(*iam.Group) setUsers []string setGroups []string resultUsers []string resultGroups []string wantErr bool }{ { name: "Set user on empty group", setup: func(r *iam.Group) {}, setUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Set user on populated group", setup: func(r *iam.Group) { iam.TestGroupMember(t, conn, r.GetPublicId(), users[0].GetPublicId()) }, setUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Set duplicate user on populated group", setup: func(r *iam.Group) { iam.TestGroupMember(t, conn, r.GetPublicId(), users[0].GetPublicId()) }, setUsers: []string{users[1].GetPublicId(), users[1].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Set empty on populated group", setup: func(r *iam.Group) { iam.TestGroupMember(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestGroupMember(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, setUsers: []string{}, resultUsers: nil, }, } for _, tc := range setCases { for _, scp := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scp.GetType(), func(t *testing.T) { grp := iam.TestGroup(t, conn, scp.GetPublicId()) tc.setup(grp) req := &pbs.SetGroupMembersRequest{ Id: grp.GetPublicId(), Version: grp.GetVersion(), MemberIds: tc.setUsers, } got, err := s.SetGroupMembers(auth.DisabledAuthTestContext(repoFn, scp.GetPublicId()), req) if tc.wantErr { assert.Error(t, err) return } s, ok := status.FromError(err) require.True(t, ok) require.NoError(t, err, "Got error: %v", s) assert.True(t, equalMembers(got.GetItem(), append(tc.resultUsers, tc.resultGroups...))) }) } } grp := iam.TestGroup(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.SetGroupMembersRequest err error }{ { name: "Bad Group Id", req: &pbs.SetGroupMembersRequest{ Id: "bad id", Version: grp.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalid user id in member list", req: &pbs.SetGroupMembersRequest{ Id: grp.GetPublicId(), Version: grp.GetVersion(), MemberIds: []string{"invalid"}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "u_recovery", req: &pbs.SetGroupMembersRequest{ Id: grp.GetPublicId(), Version: grp.GetVersion(), MemberIds: []string{globals.RecoveryUserId}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.SetGroupMembers(auth.DisabledAuthTestContext(repoFn, grp.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "SetGroupMembers(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } } func TestRemoveMember(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := groups.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new grp service.") o, p := iam.TestScopes(t, iamRepo) users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), } addCases := []struct { name string setup func(*iam.Group) removeUsers []string removeGroups []string resultUsers []string resultGroups []string wantErr bool }{ { name: "Remove user on empty group", setup: func(r *iam.Group) {}, removeUsers: []string{users[1].GetPublicId()}, wantErr: true, }, { name: "Remove 1 of 2 users from group", setup: func(r *iam.Group) { iam.TestGroupMember(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestGroupMember(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, removeUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[0].GetPublicId()}, }, { name: "Remove all users from group", setup: func(r *iam.Group) { iam.TestGroupMember(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestGroupMember(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, removeUsers: []string{users[0].GetPublicId(), users[1].GetPublicId()}, resultUsers: []string{}, }, { name: "Remove duplicate user from group", setup: func(r *iam.Group) { iam.TestGroupMember(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestGroupMember(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, removeUsers: []string{users[0].GetPublicId(), users[0].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Remove empty on populated group", setup: func(r *iam.Group) { iam.TestGroupMember(t, conn, r.GetPublicId(), users[0].GetPublicId()) }, wantErr: true, }, } for _, tc := range addCases { for _, scp := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scp.GetType(), func(t *testing.T) { grp := iam.TestGroup(t, conn, scp.GetPublicId()) tc.setup(grp) req := &pbs.RemoveGroupMembersRequest{ Id: grp.GetPublicId(), Version: grp.GetVersion(), MemberIds: tc.removeUsers, } got, err := s.RemoveGroupMembers(auth.DisabledAuthTestContext(repoFn, scp.GetPublicId()), req) if tc.wantErr { assert.Error(t, err) return } s, ok := status.FromError(err) require.True(t, ok) require.NoError(t, err, "Got error: %v", s) assert.True(t, equalMembers(got.GetItem(), tc.resultUsers)) }) } } grp := iam.TestGroup(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.RemoveGroupMembersRequest err error }{ { name: "Bad Group Id", req: &pbs.RemoveGroupMembersRequest{ Id: "bad id", Version: grp.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalid user id in member list", req: &pbs.RemoveGroupMembersRequest{ Id: grp.GetPublicId(), Version: grp.GetVersion(), MemberIds: []string{"invalid"}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.RemoveGroupMembers(auth.DisabledAuthTestContext(repoFn, grp.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "RemoveGroupMembers(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } }