From f2e82d57ac5f4f14c4dde3004dce5cf3d217a822 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:29:51 +0000 Subject: [PATCH] fix: adding code to test ListUsers with and without permission_check_v2 flag set (#9383) # Which Problems Are Solved Enhancing `v2/ListUsers()` tests by adding code to run all test with and without `permission_check_v2` flag set # Additional Context - Closes https://github.com/zitadel/zitadel/issues/9356 --------- Co-authored-by: Iraq Jaber --- internal/api/grpc/admin/org.go | 3 - .../user/v2/integration_test/query_test.go | 652 +++++++++++------- .../v2beta/integration_test/query_test.go | 345 ++++++--- 3 files changed, 645 insertions(+), 355 deletions(-) diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index f788bb5f5a..93e6936d42 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -112,9 +112,6 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain str if err != nil { return nil, err } - if err != nil { - return nil, err - } userIDs := make([]string, len(users.Users)) for i, user := range users.Users { userIDs[i] = user.ID diff --git a/internal/api/grpc/user/v2/integration_test/query_test.go b/internal/api/grpc/user/v2/integration_test/query_test.go index ada9affcbb..bbf8c24865 100644 --- a/internal/api/grpc/user/v2/integration_test/query_test.go +++ b/internal/api/grpc/user/v2/integration_test/query_test.go @@ -4,6 +4,7 @@ package user_test import ( "context" + "errors" "fmt" "slices" "testing" @@ -16,11 +17,61 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) +var ( + permissionCheckV2SetFlagInital bool + permissionCheckV2SetFlag bool +) + +type permissionCheckV2SettingsStruct struct { + TestNamePrependString string + SetFlag bool +} + +var permissionCheckV2Settings []permissionCheckV2SettingsStruct = []permissionCheckV2SettingsStruct{ + { + SetFlag: false, + TestNamePrependString: "permission_check_v2 IS NOT SET" + " ", + }, + { + SetFlag: true, + TestNamePrependString: "permission_check_v2 IS SET" + " ", + }, +} + +func setPermissionCheckV2Flag(t *testing.T, setFlag bool) { + if permissionCheckV2SetFlagInital && permissionCheckV2SetFlag == setFlag { + return + } + + _, err := Instance.Client.FeatureV2.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: &setFlag, + }) + require.NoError(t, err) + + var flagSet bool + for i := 0; !flagSet || i < 6; i++ { + res, err := Instance.Client.FeatureV2.GetInstanceFeatures(IamCTX, &feature.GetInstanceFeaturesRequest{}) + require.NoError(t, err) + if res.PermissionCheckV2.Enabled == setFlag { + flagSet = true + continue + } + time.Sleep(10 * time.Second) + } + + if !flagSet { + require.NoError(t, errors.New("unable to set permission_check_v2 flag")) + } + permissionCheckV2SetFlagInital = true + permissionCheckV2SetFlag = setFlag +} + func TestServer_GetUserByID(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { @@ -372,6 +423,11 @@ func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) } func TestServer_ListUsers(t *testing.T) { + defer func() { + _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { ctx context.Context @@ -470,13 +526,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, @@ -516,13 +570,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id, passwordChangeRequired, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, true) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, @@ -564,13 +616,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id multiple, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs())) return infos }, @@ -623,7 +673,8 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, - }, { + }, + { State: user.UserState_USER_STATE_ACTIVE, Type: &user.User_Human{ Human: &user.HumanUser{ @@ -651,13 +702,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by username, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, UsernameQuery(info.Username)) return []userAttr{info} }, @@ -697,13 +746,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username})) return []userAttr{info} }, @@ -741,189 +788,12 @@ func TestServer_ListUsers(t *testing.T) { }, { name: "list user in emails multiple, ok", - args: args{ - IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - infos := createUsers(ctx, orgResp.OrganizationId, 3, false) - request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) - return infos - }, - }, - want: &user.ListUsersResponse{ - Details: &object.ListDetails{ - TotalResult: 3, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*user.User{ - { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: "Mickey", - FamilyName: "Mouse", - NickName: gu.Ptr("Mickey"), - DisplayName: gu.Ptr("Mickey Mouse"), - PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), - }, - Email: &user.HumanEmail{ - IsVerified: true, - }, - Phone: &user.HumanPhone{ - IsVerified: true, - }, - }, - }, - }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: "Mickey", - FamilyName: "Mouse", - NickName: gu.Ptr("Mickey"), - DisplayName: gu.Ptr("Mickey Mouse"), - PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), - }, - Email: &user.HumanEmail{ - IsVerified: true, - }, - Phone: &user.HumanPhone{ - IsVerified: true, - }, - }, - }, - }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: "Mickey", - FamilyName: "Mouse", - NickName: gu.Ptr("Mickey"), - DisplayName: gu.Ptr("Mickey Mouse"), - PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), - }, - Email: &user.HumanEmail{ - IsVerified: true, - }, - Phone: &user.HumanPhone{ - IsVerified: true, - }, - }, - }, - }, - }, - }, - }, - { - name: "list user in emails no found, ok", - args: args{ - IamCTX, - &user.ListUsersRequest{Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - InUserEmailsQuery([]string{"notfound"}), - }, - }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - return []userAttr{} - }, - }, - want: &user.ListUsersResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*user.User{}, - }, - }, - { - name: "list user phone, ok", - args: args{ - IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - info := createUser(ctx, orgResp.OrganizationId, false) - request.Queries = append(request.Queries, PhoneQuery(info.Phone)) - return []userAttr{info} - }, - }, - want: &user.ListUsersResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*user.User{ - { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: "Mickey", - FamilyName: "Mouse", - NickName: gu.Ptr("Mickey"), - DisplayName: gu.Ptr("Mickey Mouse"), - PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), - }, - Email: &user.HumanEmail{ - IsVerified: true, - }, - Phone: &user.HumanPhone{ - IsVerified: true, - }, - }, - }, - }, - }, - }, - }, - { - name: "list user in emails no found, ok", - args: args{ - IamCTX, - &user.ListUsersRequest{Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - InUserEmailsQuery([]string{"notfound"}), - }, - }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - return []userAttr{} - }, - }, - want: &user.ListUsersResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*user.User{}, - }, - }, - { - name: "list user resourceowner multiple, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email()) - infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) return infos @@ -1000,93 +870,355 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, + { + name: "list user in emails no found, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, + }, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{}, + }, + }, + { + name: "list user phone, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) + request.Queries = append(request.Queries, PhoneQuery(info.Phone)) + return []userAttr{info} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user in emails no found, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, + }, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{}, + }, + }, + { + name: "list user resourceowner multiple, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email()) + + infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) + request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) + return infos + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 3, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user with org query", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + info := createUser(ctx, orgRespForOrgTests.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId)) + return []userAttr{info, {}} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 2, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, + // this is the admin of the org craated in Instance.CreateOrganization() + nil, + }, + }, + }, + { + name: "list user with wrong org query", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + // info := createUser(ctx, orgRespForOrgTests.OrganizationId, false) + createUser(ctx, orgRespForOrgTests.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests2.OrganizationId)) + return []userAttr{{}} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + // this is the admin of the org craated in Instance.CreateOrganization() + nil, + }, + }, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - infos := tt.args.dep(IamCTX, tt.args.req) + for _, f := range permissionCheckV2Settings { + f := f + for _, tt := range tests { + t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) { + setPermissionCheckV2Flag(t, f.SetFlag) + infos := tt.args.dep(IamCTX, tt.args.req) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.ListUsers(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - require.NoError(ttt, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListUsers(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + require.NoError(ttt, err) - // always only give back dependency infos which are required for the response - require.Len(ttt, tt.want.Result, len(infos)) - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions - tt.want.Details.TotalResult = got.Details.TotalResult + // always only give back dependency infos which are required for the response + require.Len(ttt, tt.want.Result, len(infos)) + if assert.Len(ttt, got.Result, len(tt.want.Result)) { + tt.want.Details.TotalResult = got.Details.TotalResult - // fill in userid and username as it is generated - for i := range infos { - tt.want.Result[i].UserId = infos[i].UserID - tt.want.Result[i].Username = infos[i].Username - tt.want.Result[i].PreferredLoginName = infos[i].Username - tt.want.Result[i].LoginNames = []string{infos[i].Username} - if human := tt.want.Result[i].GetHuman(); human != nil { - human.Email.Email = infos[i].Username - human.Phone.Phone = infos[i].Phone - if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { - human.PasswordChanged = infos[i].Changed + // fill in userid and username as it is generated + for i := range infos { + if tt.want.Result[i] == nil { + continue } + tt.want.Result[i].UserId = infos[i].UserID + tt.want.Result[i].Username = infos[i].Username + tt.want.Result[i].PreferredLoginName = infos[i].Username + tt.want.Result[i].LoginNames = []string{infos[i].Username} + if human := tt.want.Result[i].GetHuman(); human != nil { + human.Email.Email = infos[i].Username + human.Phone.Phone = infos[i].Phone + if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { + human.PasswordChanged = infos[i].Changed + } + } + tt.want.Result[i].Details = infos[i].Details + } + for i := range tt.want.Result { + if tt.want.Result[i] == nil { + continue + } + assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) } - tt.want.Result[i].Details = infos[i].Details } - for i := range tt.want.Result { - assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) - } - } - integration.AssertListDetails(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected user result") - }) + integration.AssertListDetails(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected user result") + }) + } } } func InUserIDsQuery(ids []string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_InUserIdsQuery{ - InUserIdsQuery: &user.InUserIDQuery{ - UserIds: ids, + return &user.SearchQuery{ + Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{ + UserIds: ids, + }, }, - }, } } func InUserEmailsQuery(emails []string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_InUserEmailsQuery{ - InUserEmailsQuery: &user.InUserEmailsQuery{ - UserEmails: emails, + return &user.SearchQuery{ + Query: &user.SearchQuery_InUserEmailsQuery{ + InUserEmailsQuery: &user.InUserEmailsQuery{ + UserEmails: emails, + }, }, - }, } } func PhoneQuery(number string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_PhoneQuery{ - PhoneQuery: &user.PhoneQuery{ - Number: number, + return &user.SearchQuery{ + Query: &user.SearchQuery_PhoneQuery{ + PhoneQuery: &user.PhoneQuery{ + Number: number, + }, }, - }, } } func UsernameQuery(username string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{ - UserNameQuery: &user.UserNameQuery{ - UserName: username, + return &user.SearchQuery{ + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: username, + }, }, - }, } } func OrganizationIdQuery(resourceowner string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_OrganizationIdQuery{ - OrganizationIdQuery: &user.OrganizationIdQuery{ - OrganizationId: resourceowner, + return &user.SearchQuery{ + Query: &user.SearchQuery_OrganizationIdQuery{ + OrganizationIdQuery: &user.OrganizationIdQuery{ + OrganizationId: resourceowner, + }, }, - }, } } diff --git a/internal/api/grpc/user/v2beta/integration_test/query_test.go b/internal/api/grpc/user/v2beta/integration_test/query_test.go index 61b0d829ff..73bff3fd0d 100644 --- a/internal/api/grpc/user/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/query_test.go @@ -4,6 +4,7 @@ package user_test import ( "context" + "errors" "fmt" "slices" "testing" @@ -16,6 +17,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" "github.com/zitadel/zitadel/pkg/grpc/object/v2" object_v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" "github.com/zitadel/zitadel/pkg/grpc/session/v2" @@ -30,6 +32,55 @@ func detailsV2ToV2beta(obj *object.Details) *object_v2beta.Details { } } +var ( + permissionCheckV2SetFlagInital bool + permissionCheckV2SetFlag bool +) + +type permissionCheckV2SettingsStruct struct { + TestNamePrependString string + SetFlag bool +} + +var permissionCheckV2Settings []permissionCheckV2SettingsStruct = []permissionCheckV2SettingsStruct{ + { + SetFlag: false, + TestNamePrependString: "permission_check_v2 IS NOT SET" + " ", + }, + { + SetFlag: true, + TestNamePrependString: "permission_check_v2 IS SET" + " ", + }, +} + +func setPermissionCheckV2Flag(t *testing.T, setFlag bool) { + if permissionCheckV2SetFlagInital && permissionCheckV2SetFlag == setFlag { + return + } + + _, err := Instance.Client.FeatureV2.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: &setFlag, + }) + require.NoError(t, err) + + var flagSet bool + for i := 0; !flagSet || i < 6; i++ { + res, err := Instance.Client.FeatureV2.GetInstanceFeatures(IamCTX, &feature.GetInstanceFeaturesRequest{}) + require.NoError(t, err) + if res.PermissionCheckV2.Enabled == setFlag { + flagSet = true + continue + } + time.Sleep(10 * time.Second) + } + + if !flagSet { + require.NoError(t, errors.New("unable to set permission_check_v2 flag")) + } + permissionCheckV2SetFlagInital = true + permissionCheckV2SetFlag = setFlag +} + func TestServer_GetUserByID(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { @@ -382,6 +433,11 @@ func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) } func TestServer_ListUsers(t *testing.T) { + defer func() { + _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { ctx context.Context @@ -480,13 +536,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, @@ -526,13 +580,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id, passwordChangeRequired, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, true) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, @@ -574,13 +626,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id multiple, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs())) return infos }, @@ -612,7 +662,8 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, - }, { + }, + { State: user.UserState_USER_STATE_ACTIVE, Type: &user.User_Human{ Human: &user.HumanUser{ @@ -632,7 +683,8 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, - }, { + }, + { State: user.UserState_USER_STATE_ACTIVE, Type: &user.User_Human{ Human: &user.HumanUser{ @@ -660,13 +712,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by username, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, UsernameQuery(info.Username)) return []userAttr{info} }, @@ -713,6 +763,7 @@ func TestServer_ListUsers(t *testing.T) { }, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username})) return []userAttr{info} }, @@ -752,13 +803,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails multiple, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) return infos }, @@ -838,10 +887,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails no found, ok", args: args{ IamCTX, - &user.ListUsersRequest{Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - InUserEmailsQuery([]string{"notfound"}), - }, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, }, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { return []userAttr{} @@ -860,13 +910,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user phone, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, PhoneQuery(info.Phone)) return []userAttr{info} }, @@ -902,6 +950,29 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, + { + name: "list user in emails no found, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, + }, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{} + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{}, + }, + }, { name: "list user resourceowner multiple, ok", args: args{ @@ -911,6 +982,7 @@ func TestServer_ListUsers(t *testing.T) { orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email()) infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) return infos @@ -987,93 +1059,182 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, + { + name: "list user with org query", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + info := createUser(ctx, orgRespForOrgTests.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId)) + return []userAttr{info, {}} + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 2, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, + // this is the admin of the org craated in Instance.CreateOrganization() + nil, + }, + }, + }, + { + name: "list user with wrong org query", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + // info := createUser(ctx, orgRespForOrgTests.OrganizationId, false) + createUser(ctx, orgRespForOrgTests.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests2.OrganizationId)) + return []userAttr{{}} + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + // this is the admin of the org craated in Instance.CreateOrganization() + nil, + }, + }, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - infos := tt.args.dep(IamCTX, tt.args.req) + for _, f := range permissionCheckV2Settings { + f := f + for _, tt := range tests { + t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) { + setPermissionCheckV2Flag(t, f.SetFlag) + infos := tt.args.dep(IamCTX, tt.args.req) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.ListUsers(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - require.NoError(ttt, err) + // retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 10*time.Minute) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 20*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListUsers(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + require.NoError(ttt, err) - // always only give back dependency infos which are required for the response - require.Len(ttt, tt.want.Result, len(infos)) - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions - tt.want.Details.TotalResult = got.Details.TotalResult + // always only give back dependency infos which are required for the response + require.Len(ttt, tt.want.Result, len(infos)) + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Result, len(tt.want.Result)) { + // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions + tt.want.Details.TotalResult = got.Details.TotalResult - // fill in userid and username as it is generated - for i := range infos { - tt.want.Result[i].UserId = infos[i].UserID - tt.want.Result[i].Username = infos[i].Username - tt.want.Result[i].PreferredLoginName = infos[i].Username - tt.want.Result[i].LoginNames = []string{infos[i].Username} - if human := tt.want.Result[i].GetHuman(); human != nil { - human.Email.Email = infos[i].Username - human.Phone.Phone = infos[i].Phone - if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { - human.PasswordChanged = infos[i].Changed + // fill in userid and username as it is generated + for i := range infos { + if tt.want.Result[i] == nil { + continue } + tt.want.Result[i].UserId = infos[i].UserID + tt.want.Result[i].Username = infos[i].Username + tt.want.Result[i].PreferredLoginName = infos[i].Username + tt.want.Result[i].LoginNames = []string{infos[i].Username} + if human := tt.want.Result[i].GetHuman(); human != nil { + human.Email.Email = infos[i].Username + human.Phone.Phone = infos[i].Phone + if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { + human.PasswordChanged = infos[i].Changed + } + } + tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details) + } + for i := range tt.want.Result { + if tt.want.Result[i] == nil { + continue + } + assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) } - tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details) } - for i := range tt.want.Result { - assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) - } - } - integration.AssertListDetails(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected user result") - }) + integration.AssertListDetails(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected user result") + }) + } } } func InUserIDsQuery(ids []string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_InUserIdsQuery{ - InUserIdsQuery: &user.InUserIDQuery{ - UserIds: ids, + return &user.SearchQuery{ + Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{ + UserIds: ids, + }, }, - }, } } func InUserEmailsQuery(emails []string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_InUserEmailsQuery{ - InUserEmailsQuery: &user.InUserEmailsQuery{ - UserEmails: emails, + return &user.SearchQuery{ + Query: &user.SearchQuery_InUserEmailsQuery{ + InUserEmailsQuery: &user.InUserEmailsQuery{ + UserEmails: emails, + }, }, - }, } } func PhoneQuery(number string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_PhoneQuery{ - PhoneQuery: &user.PhoneQuery{ - Number: number, + return &user.SearchQuery{ + Query: &user.SearchQuery_PhoneQuery{ + PhoneQuery: &user.PhoneQuery{ + Number: number, + }, }, - }, } } func UsernameQuery(username string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{ - UserNameQuery: &user.UserNameQuery{ - UserName: username, + return &user.SearchQuery{ + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: username, + }, }, - }, } } func OrganizationIdQuery(resourceowner string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_OrganizationIdQuery{ - OrganizationIdQuery: &user.OrganizationIdQuery{ - OrganizationId: resourceowner, + return &user.SearchQuery{ + Query: &user.SearchQuery_OrganizationIdQuery{ + OrganizationIdQuery: &user.OrganizationIdQuery{ + OrganizationId: resourceowner, + }, }, - }, } }