//go:build integration package user_test import ( "context" "encoding/base64" "fmt" "slices" "testing" "time" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/filter/v2" v2 "github.com/zitadel/zitadel/pkg/grpc/metadata/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" ) func TestServer_GetUserByID(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), integration.Email()) type args struct { ctx context.Context req *user.GetUserByIDRequest dep func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr } tests := []struct { name string args args want *user.GetUserByIDResponse wantErr bool }{ { name: "user by ID, no id provided", args: args{ IamCTX, &user.GetUserByIDRequest{ UserId: "", }, func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr { return nil }, }, wantErr: true, }, { name: "user by ID, not found", args: args{ IamCTX, &user.GetUserByIDRequest{ UserId: "unknown", }, func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr { return nil }, }, wantErr: true, }, { name: "user by ID, ok", args: args{ IamCTX, &user.GetUserByIDRequest{}, func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr { info := createUser(ctx, Instance, orgResp.OrganizationId, false) request.UserId = info.UserID return &info }, }, want: &user.GetUserByIDResponse{ User: &user.User{ State: user.UserState_USER_STATE_ACTIVE, Username: "", LoginNames: nil, PreferredLoginName: "", 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(), AvatarUrl: "", }, Email: &user.HumanEmail{ IsVerified: true, }, Phone: &user.HumanPhone{ IsVerified: true, }, }, }, }, Details: &object.Details{ ChangeDate: timestamppb.Now(), CreationDate: timestamppb.Now(), ResourceOwner: orgResp.OrganizationId, }, }, }, { name: "user by ID, passwordChangeRequired, ok", args: args{ IamCTX, &user.GetUserByIDRequest{}, func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr { info := createUser(ctx, Instance, orgResp.OrganizationId, true) request.UserId = info.UserID return &info }, }, want: &user.GetUserByIDResponse{ User: &user.User{ State: user.UserState_USER_STATE_ACTIVE, Username: "", LoginNames: nil, PreferredLoginName: "", 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(), AvatarUrl: "", }, Email: &user.HumanEmail{ IsVerified: true, }, Phone: &user.HumanPhone{ IsVerified: true, }, PasswordChangeRequired: true, PasswordChanged: timestamppb.Now(), }, }, }, Details: &object.Details{ ChangeDate: timestamppb.Now(), CreationDate: timestamppb.Now(), ResourceOwner: orgResp.OrganizationId, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { userAttr := 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.GetUserByID(tt.args.ctx, tt.args.req) if tt.wantErr { assert.Error(ttt, err) return } if !assert.NoError(ttt, err) { return } tt.want.User.Details = userAttr.Details tt.want.User.UserId = userAttr.UserID tt.want.User.Username = userAttr.Username tt.want.User.PreferredLoginName = userAttr.Username tt.want.User.LoginNames = []string{userAttr.Username} if human := tt.want.User.GetHuman(); human != nil { human.Email.Email = userAttr.Username human.Phone.Phone = userAttr.Phone if tt.want.User.GetHuman().GetPasswordChanged() != nil { human.PasswordChanged = userAttr.Changed } } assert.EqualExportedValues(ttt, tt.want.User, got.User) integration.AssertDetails(ttt, tt.want, got) }, retryDuration, tick) }) } } func TestServer_GetUserByID_Permission(t *testing.T) { newOrgOwnerEmail := integration.Email() newOrg := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() type args struct { ctx context.Context req *user.GetUserByIDRequest } tests := []struct { name string args args want *user.GetUserByIDResponse wantErr bool }{ { name: "System, ok", args: args{ SystemCTX, &user.GetUserByIDRequest{ UserId: newUserID, }, }, want: &user.GetUserByIDResponse{ User: &user.User{ State: user.UserState_USER_STATE_ACTIVE, Username: "", LoginNames: nil, PreferredLoginName: "", Type: &user.User_Human{ Human: &user.HumanUser{ Profile: &user.HumanProfile{ GivenName: "firstname", FamilyName: "lastname", NickName: gu.Ptr(""), DisplayName: gu.Ptr("firstname lastname"), PreferredLanguage: gu.Ptr("und"), Gender: user.Gender_GENDER_UNSPECIFIED.Enum(), AvatarUrl: "", }, Email: &user.HumanEmail{ Email: newOrgOwnerEmail, }, Phone: &user.HumanPhone{}, }, }, }, Details: &object.Details{ ChangeDate: timestamppb.Now(), CreationDate: timestamppb.Now(), ResourceOwner: newOrg.GetOrganizationId(), }, }, }, { name: "Instance, ok", args: args{ IamCTX, &user.GetUserByIDRequest{ UserId: newUserID, }, }, want: &user.GetUserByIDResponse{ User: &user.User{ State: user.UserState_USER_STATE_ACTIVE, Username: "", LoginNames: nil, PreferredLoginName: "", Type: &user.User_Human{ Human: &user.HumanUser{ Profile: &user.HumanProfile{ GivenName: "firstname", FamilyName: "lastname", NickName: gu.Ptr(""), DisplayName: gu.Ptr("firstname lastname"), PreferredLanguage: gu.Ptr("und"), Gender: user.Gender_GENDER_UNSPECIFIED.Enum(), AvatarUrl: "", }, Email: &user.HumanEmail{ Email: newOrgOwnerEmail, }, Phone: &user.HumanPhone{}, }, }, }, Details: &object.Details{ ChangeDate: timestamppb.Now(), CreationDate: timestamppb.Now(), ResourceOwner: newOrg.GetOrganizationId(), }, }, }, { name: "Org, error", args: args{ OrgCTX, &user.GetUserByIDRequest{ UserId: newUserID, }, }, wantErr: true, }, { name: "User, error", args: args{ UserCTX, &user.GetUserByIDRequest{ UserId: newUserID, }, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { got, err := Client.GetUserByID(tt.args.ctx, tt.args.req) if tt.wantErr { assert.Error(ttt, err) return } if !assert.NoError(ttt, err) { return } tt.want.User.UserId = tt.args.req.GetUserId() tt.want.User.Username = newOrgOwnerEmail tt.want.User.PreferredLoginName = newOrgOwnerEmail tt.want.User.LoginNames = []string{newOrgOwnerEmail} if human := tt.want.User.GetHuman(); human != nil { human.Email.Email = newOrgOwnerEmail } // details tested in GetUserByID tt.want.User.Details = got.User.GetDetails() assert.Equal(ttt, tt.want.User, got.User) }, retryDuration, tick, "timeout waiting for expected user result") }) } } type userAttrs []userAttr func (u userAttrs) userIDs() []string { ids := make([]string, len(u)) for i := range u { ids[i] = u[i].UserID } return ids } func (u userAttrs) emails() []string { emails := make([]string, len(u)) for i := range u { emails[i] = u[i].Username } return emails } type userAttr struct { UserID string Username string Phone string Changed *timestamppb.Timestamp Details *object.Details } func createUsers(ctx context.Context, instance *integration.Instance, orgID string, count int, passwordChangeRequired bool) userAttrs { infos := make([]userAttr, count) for i := 0; i < count; i++ { infos[i] = createUser(ctx, instance, orgID, passwordChangeRequired) } slices.Reverse(infos) return infos } func createUser(ctx context.Context, instance *integration.Instance, orgID string, passwordChangeRequired bool) userAttr { username := integration.Email() return createUserWithUserName(ctx, instance, username, orgID, passwordChangeRequired) } func createUserWithUserName(ctx context.Context, instance *integration.Instance, username string, orgID string, passwordChangeRequired bool) userAttr { // used as default country prefix phone := integration.Phone() resp := instance.CreateHumanUserVerified(ctx, orgID, username, phone) info := userAttr{resp.GetUserId(), username, phone, nil, resp.GetDetails()} // as the change date of the creation is the creation date resp.Details.CreationDate = resp.GetDetails().GetChangeDate() if passwordChangeRequired { details := instance.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true) info.Changed = details.GetChangeDate() } return info } func TestServer_ListUsers(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), integration.Email()) type args struct { ctx context.Context req *user.ListUsersRequest dep func(ctx context.Context, request *user.ListUsersRequest) userAttrs } tt := []struct { name string args args want *user.ListUsersResponse wantErr bool }{ { name: "list user by id, no permission machine user", args: args{ UserCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, Instance, orgResp.OrganizationId, false) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 0, Timestamp: timestamppb.Now(), }, SortingColumn: 0, Result: []*user.User{}, }, }, { name: "list user by id, no permission human user", args: func() args { info := createUser(IamCTX, Instance, orgResp.OrganizationId, true) // create session to get token userID := info.UserID createResp, err := Instance.Client.SessionV2.CreateSession(IamCTX, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ Search: &session.CheckUser_UserId{UserId: userID}, }, Password: &session.CheckPassword{ Password: integration.UserPassword, }, }, }) if err != nil { require.NoError(t, err) } // use token to get ctx HumanCTX := integration.WithAuthorizationToken(IamCTX, createResp.GetSessionToken()) return args{ HumanCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { return []userAttr{info} }, } }(), want: &user.ListUsersResponse{ // human user should return itself when calling ListUsers() even if it has no permissions 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, }, PasswordChangeRequired: true, PasswordChanged: timestamppb.Now(), }, }, }, }, }, }, { name: "list user by id, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, Instance, 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} }, }, 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 by id, passwordChangeRequired, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, Instance, 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} }, }, 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, }, PasswordChangeRequired: true, PasswordChanged: timestamppb.Now(), }, }, }, }, }, }, { name: "list user by id and meta key multiple, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, Instance, orgResp.OrganizationId, 3, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs())) Instance.SetUserMetadata(ctx, infos[0].UserID, "my meta", "my value 1") Instance.SetUserMetadata(ctx, infos[1].UserID, "my meta 2", "my value 3") Instance.SetUserMetadata(ctx, infos[2].UserID, "my meta", "my value 2") request.Queries = append(request.Queries, MetadataKeyContainsQuery("my meta")) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return infos }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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, }, }, }, }, }, }, }, { // https://github.com/zitadel/zitadel/issues/10825 name: "list user with metadata by IDs with offset and limit, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, Instance, orgResp.OrganizationId, 3, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs())) // With the original bug, this would multiply the "TotalResult" by 2. for _, user := range infos { for i := 0; i < 2; i++ { Instance.SetUserMetadata(ctx, user.UserID, fmt.Sprintf("key-%d", i), fmt.Sprintf("value-%d", i)) } } infos = infos[1:] // prevent panic in user ID setting below request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE request.Query = &object.ListQuery{ Offset: 1, Limit: 2, } return infos }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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, }, }, }, }, }, }, }, { name: "list user by username, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, Instance, orgResp.OrganizationId, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, UsernameQuery(info.Username)) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return []userAttr{info} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, Instance, orgResp.OrganizationId, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username})) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return []userAttr{info} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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 by emails and meta value multiple, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, Instance, orgResp.OrganizationId, 3, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) Instance.SetUserMetadata(ctx, infos[0].UserID, "my meta 1", "my value") Instance.SetUserMetadata(ctx, infos[0].UserID, "my meta 2", "my value") Instance.SetUserMetadata(ctx, infos[1].UserID, "my meta 2", "my value") Instance.SetUserMetadata(ctx, infos[2].UserID, "my meta", "my value") request.Queries = append(request.Queries, MetadataValueQuery("my value")) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return infos }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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, }, }, }, }, }, }, }, { // https://github.com/zitadel/zitadel/issues/10825 name: "list user with metadata by emails with offset and limit, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, Instance, orgResp.OrganizationId, 3, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) // With the original bug, this would multiply the "TotalResult" by 2. for _, user := range infos { for i := 0; i < 2; i++ { Instance.SetUserMetadata(ctx, user.UserID, fmt.Sprintf("key-%d", i), fmt.Sprintf("value-%d", i)) } } infos = infos[1:] // prevent panic in user ID setting below request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE request.Query = &object.ListQuery{ Offset: 1, Limit: 2, } return infos }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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, }, }, }, }, }, }, }, { 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, Instance, 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, integration.OrganizationName(), integration.Email()) infos := createUsers(ctx, Instance, orgResp.OrganizationId, 3, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return infos }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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, integration.OrganizationName(), integration.Email()) info := createUser(ctx, Instance, orgRespForOrgTests.OrganizationId, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId)) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return []userAttr{info, {}} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 2, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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, integration.OrganizationName(), integration.Email()) orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), integration.Email()) createUser(ctx, Instance, 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, }, }, }, { name: "when no users matching meta key should return empty list", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { createUser(ctx, Instance, orgResp.OrganizationId, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, MetadataKeyContainsQuery("some non-existent meta")) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return []userAttr{} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 0, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, Result: []*user.User{}, }, }, } for _, tc := range tt { t.Run(tc.name, func(t1 *testing.T) { infos := tc.args.dep(IamCTX, tc.args.req) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.args.ctx, time.Minute) require.EventuallyWithT(t1, func(ttt *assert.CollectT) { got, err := Client.ListUsers(tc.args.ctx, tc.args.req) if tc.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, tc.want.Result, len(infos)) if assert.Len(ttt, got.Result, len(tc.want.Result)) { tc.want.Details.TotalResult = got.Details.TotalResult // fill in userid and username as it is generated for i := range infos { if tc.want.Result[i] == nil { continue } tc.want.Result[i].UserId = infos[i].UserID tc.want.Result[i].Username = infos[i].Username tc.want.Result[i].PreferredLoginName = infos[i].Username tc.want.Result[i].LoginNames = []string{infos[i].Username} if human := tc.want.Result[i].GetHuman(); human != nil { human.Email.Email = infos[i].Username human.Phone.Phone = infos[i].Phone if tc.want.Result[i].GetHuman().GetPasswordChanged() != nil { human.PasswordChanged = infos[i].Changed } } tc.want.Result[i].Details = infos[i].Details } for i := range tc.want.Result { if tc.want.Result[i] == nil { continue } assert.EqualExportedValues(ttt, got.Result[i], tc.want.Result[i]) } } integration.AssertListDetails(ttt, tc.want, got) }, retryDuration, tick, "timeout waiting for expected user result") }) } } func TestServer_ListUsers_PermissionsV2(t *testing.T) { ensureFeaturePermissionV2Enabled(t, InstancePermissionV2) iamOwnerCtx := InstancePermissionV2.WithAuthorizationToken(OrgCTX, integration.UserTypeIAMOwner) orgResp := InstancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) type args struct { ctx context.Context req *user.ListUsersRequest dep func(ctx context.Context, request *user.ListUsersRequest) userAttrs } tt := []struct { name string args args want *user.ListUsersResponse wantErr bool }{ { name: "list user by id, no permission machine user", args: args{ InstancePermissionV2.WithAuthorizationToken(OrgCTX, integration.UserTypeNoPermission), &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, InstancePermissionV2, orgResp.OrganizationId, false) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 0, Timestamp: timestamppb.Now(), }, SortingColumn: 0, Result: []*user.User{}, }, }, { name: "list user by id, no permission human user", args: func() args { info := createUser(iamOwnerCtx, InstancePermissionV2, orgResp.OrganizationId, true) // create session to get token userID := info.UserID createResp, err := InstancePermissionV2.Client.SessionV2.CreateSession(iamOwnerCtx, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ Search: &session.CheckUser_UserId{UserId: userID}, }, Password: &session.CheckPassword{ Password: integration.UserPassword, }, }, }) if err != nil { require.NoError(t, err) } // use token to get ctx HumanCTX := integration.WithAuthorizationToken(iamOwnerCtx, createResp.GetSessionToken()) return args{ HumanCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { return []userAttr{info} }, } }(), want: &user.ListUsersResponse{ // human user should return itself when calling ListUsers() even if it has no permissions 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, }, PasswordChangeRequired: true, PasswordChanged: timestamppb.Now(), }, }, }, }, }, }, { name: "list user by id, ok", args: args{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, InstancePermissionV2, 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} }, }, 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 by id, passwordChangeRequired, ok", args: args{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, InstancePermissionV2, 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} }, }, 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, }, PasswordChangeRequired: true, PasswordChanged: timestamppb.Now(), }, }, }, }, }, }, { name: "list user by id and meta key multiple, ok", args: args{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, InstancePermissionV2, orgResp.OrganizationId, 3, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs())) InstancePermissionV2.SetUserMetadata(ctx, infos[0].UserID, "my meta", "my value 1") InstancePermissionV2.SetUserMetadata(ctx, infos[1].UserID, "my meta 2", "my value 3") InstancePermissionV2.SetUserMetadata(ctx, infos[2].UserID, "my meta", "my value 2") request.Queries = append(request.Queries, MetadataKeyContainsQuery("my meta")) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return infos }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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 by username, ok", args: args{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, InstancePermissionV2, orgResp.OrganizationId, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, UsernameQuery(info.Username)) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return []userAttr{info} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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, ok", args: args{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, InstancePermissionV2, orgResp.OrganizationId, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username})) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return []userAttr{info} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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 by emails and meta value multiple, ok", args: args{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, InstancePermissionV2, orgResp.OrganizationId, 3, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) InstancePermissionV2.SetUserMetadata(ctx, infos[0].UserID, "my meta 1", "my value") InstancePermissionV2.SetUserMetadata(ctx, infos[0].UserID, "my meta 2", "my value") InstancePermissionV2.SetUserMetadata(ctx, infos[1].UserID, "my meta 2", "my value") InstancePermissionV2.SetUserMetadata(ctx, infos[2].UserID, "my meta", "my value") request.Queries = append(request.Queries, MetadataValueQuery("my value")) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return infos }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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{ iamOwnerCtx, &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{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, InstancePermissionV2, 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{ iamOwnerCtx, &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{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { orgResp := InstancePermissionV2.CreateOrganization(ctx, integration.OrganizationName(), integration.Email()) infos := createUsers(ctx, InstancePermissionV2, orgResp.OrganizationId, 3, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return infos }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { orgRespForOrgTests := InstancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) info := createUser(ctx, InstancePermissionV2, orgRespForOrgTests.OrganizationId, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId)) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return []userAttr{info, {}} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 2, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, 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 InstancePermissionV2.CreateOrganization() nil, }, }, }, { name: "list user with wrong org query", args: args{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { orgRespForOrgTests := InstancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) orgRespForOrgTests2 := InstancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) createUser(ctx, InstancePermissionV2, 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 InstancePermissionV2.CreateOrganization() nil, }, }, }, { name: "when no users matching meta key should return empty list", args: args{ iamOwnerCtx, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { createUser(ctx, InstancePermissionV2, orgResp.OrganizationId, false) request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, MetadataKeyContainsQuery("some non-existent meta")) request.SortingColumn = user.UserFieldName_USER_FIELD_NAME_CREATION_DATE return []userAttr{} }, }, want: &user.ListUsersResponse{ Details: &object.ListDetails{ TotalResult: 0, Timestamp: timestamppb.Now(), }, SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, Result: []*user.User{}, }, }, } for _, tc := range tt { t.Run(tc.name, func(t1 *testing.T) { infos := tc.args.dep(iamOwnerCtx, tc.args.req) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.args.ctx, 20*time.Second) require.EventuallyWithT(t1, func(ttt *assert.CollectT) { got, err := InstancePermissionV2.Client.UserV2.ListUsers(tc.args.ctx, tc.args.req) if tc.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, tc.want.Result, len(infos)) if assert.Len(ttt, got.Result, len(tc.want.Result)) { tc.want.Details.TotalResult = got.Details.TotalResult // fill in userid and username as it is generated for i := range infos { if tc.want.Result[i] == nil { continue } tc.want.Result[i].UserId = infos[i].UserID tc.want.Result[i].Username = infos[i].Username tc.want.Result[i].PreferredLoginName = infos[i].Username tc.want.Result[i].LoginNames = []string{infos[i].Username} if human := tc.want.Result[i].GetHuman(); human != nil { human.Email.Email = infos[i].Username human.Phone.Phone = infos[i].Phone if tc.want.Result[i].GetHuman().GetPasswordChanged() != nil { human.PasswordChanged = infos[i].Changed } } tc.want.Result[i].Details = infos[i].Details } for i := range tc.want.Result { if tc.want.Result[i] == nil { continue } assert.EqualExportedValues(ttt, got.Result[i], tc.want.Result[i]) } } integration.AssertListDetails(ttt, tc.want, got) }, retryDuration, tick, "timeout waiting for expected user result") }) } } func TestServer_SystemUsers_ListUsers(t *testing.T) { org1 := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), integration.Email()) org2 := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), "org2@zitadel.com") org3 := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), integration.Email()) _ = createUserWithUserName(IamCTX, Instance, "Test_SystemUsers_ListUser1@zitadel.com", org1.OrganizationId, false) _ = createUserWithUserName(IamCTX, Instance, "Test_SystemUsers_ListUser2@zitadel.com", org2.OrganizationId, false) _ = createUserWithUserName(IamCTX, Instance, "Test_SystemUsers_ListUser3@zitadel.com", org3.OrganizationId, false) tests := []struct { name string ctx context.Context req *user.ListUsersRequest expectedFoundUsernames []string checkNumberOfUsersReturned bool }{ { name: "list users with neccessary permissions", ctx: SystemCTX, req: &user.ListUsersRequest{}, // the number of users returned will vary from test run to test run, // so just check the system user gets back users from different orgs whcih it is not a memeber of checkNumberOfUsersReturned: false, expectedFoundUsernames: []string{"Test_SystemUsers_ListUser1@zitadel.com", "Test_SystemUsers_ListUser2@zitadel.com", "Test_SystemUsers_ListUser3@zitadel.com"}, }, { name: "list users without neccessary permissions", ctx: SystemUserWithNoPermissionsCTX, req: &user.ListUsersRequest{}, // check no users returned checkNumberOfUsersReturned: true, }, { name: "list users with neccessary permissions specifying org", req: &user.ListUsersRequest{ Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, }, ctx: SystemCTX, expectedFoundUsernames: []string{"Test_SystemUsers_ListUser2@zitadel.com", "org2@zitadel.com"}, checkNumberOfUsersReturned: true, }, { name: "list users without neccessary permissions specifying org", req: &user.ListUsersRequest{ Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, }, ctx: SystemUserWithNoPermissionsCTX, // check no users returned checkNumberOfUsersReturned: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, 1*time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { got, err := Client.ListUsers(tt.ctx, tt.req) require.NoError(ttt, err) if tt.checkNumberOfUsersReturned { require.Equal(t, len(tt.expectedFoundUsernames), len(got.Result)) } if tt.expectedFoundUsernames != nil { for _, user := range got.Result { for i, username := range tt.expectedFoundUsernames { if username == user.Username { tt.expectedFoundUsernames = tt.expectedFoundUsernames[i+1:] break } } if len(tt.expectedFoundUsernames) == 0 { return } } require.FailNow(t, "unable to find all users with specified usernames") } }, retryDuration, tick, "timeout waiting for expected user result") }) } } func TestServer_SystemUsers_ListUsers_PermissionsV2(t *testing.T) { ensureFeaturePermissionV2Enabled(t, InstancePermissionV2) iamOwnerCtx := InstancePermissionV2.WithAuthorizationToken(OrgCTX, integration.UserTypeIAMOwner) org1 := InstancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) org2 := InstancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), "org2@zitadel.com") org3 := InstancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) _ = createUserWithUserName(iamOwnerCtx, InstancePermissionV2, "Test_SystemUsers_ListUser1@zitadel.com", org1.OrganizationId, false) _ = createUserWithUserName(iamOwnerCtx, InstancePermissionV2, "Test_SystemUsers_ListUser2@zitadel.com", org2.OrganizationId, false) _ = createUserWithUserName(iamOwnerCtx, InstancePermissionV2, "Test_SystemUsers_ListUser3@zitadel.com", org3.OrganizationId, false) tests := []struct { name string ctx context.Context req *user.ListUsersRequest expectedFoundUsernames []string checkNumberOfUsersReturned bool }{ { name: "list users with neccessary permissions", ctx: SystemCTX, req: &user.ListUsersRequest{}, // the number of users returned will vary from test run to test run, // so just check the system user gets back users from different orgs whcih it is not a memeber of checkNumberOfUsersReturned: false, expectedFoundUsernames: []string{"Test_SystemUsers_ListUser1@zitadel.com", "Test_SystemUsers_ListUser2@zitadel.com", "Test_SystemUsers_ListUser3@zitadel.com"}, }, { name: "list users without neccessary permissions", ctx: SystemUserWithNoPermissionsCTX, req: &user.ListUsersRequest{}, // check no users returned checkNumberOfUsersReturned: true, }, { name: "list users with neccessary permissions specifying org", req: &user.ListUsersRequest{ Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, }, ctx: SystemCTX, expectedFoundUsernames: []string{"Test_SystemUsers_ListUser2@zitadel.com", "org2@zitadel.com"}, checkNumberOfUsersReturned: true, }, { name: "list users without neccessary permissions specifying org", req: &user.ListUsersRequest{ Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, }, ctx: SystemUserWithNoPermissionsCTX, // check no users returned checkNumberOfUsersReturned: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, 1*time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { got, err := InstancePermissionV2.Client.UserV2.ListUsers(tt.ctx, tt.req) require.NoError(ttt, err) if tt.checkNumberOfUsersReturned { require.Equal(t, len(tt.expectedFoundUsernames), len(got.Result)) } if tt.expectedFoundUsernames != nil { for _, user := range got.Result { for i, username := range tt.expectedFoundUsernames { if username == user.Username { tt.expectedFoundUsernames = tt.expectedFoundUsernames[i+1:] break } } if len(tt.expectedFoundUsernames) == 0 { return } } require.FailNow(t, "unable to find all users with specified usernames") } }, 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, }, }, } } func InUserEmailsQuery(emails []string) *user.SearchQuery { 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, }, }, } } func UsernameQuery(username string) *user.SearchQuery { 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, }, }, } } func OrQuery(queries []*user.SearchQuery) *user.SearchQuery { return &user.SearchQuery{ Query: &user.SearchQuery_OrQuery{ OrQuery: &user.OrQuery{ Queries: queries, }, }, } } func MetadataKeyContainsQuery(metadataKey string) *user.SearchQuery { return &user.SearchQuery{ Query: &user.SearchQuery_MetadataKeyFilter{ MetadataKeyFilter: &v2.MetadataKeyFilter{ Key: metadataKey, Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH}, }, } } func MetakeyEqualsQuery(metaKey string) *user.SearchQuery { return &user.SearchQuery{ Query: &user.SearchQuery_MetadataKeyFilter{ MetadataKeyFilter: &v2.MetadataKeyFilter{ Key: metaKey, Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS}, }, } } func MetadataValueQuery(metaValue string) *user.SearchQuery { return &user.SearchQuery{ Query: &user.SearchQuery_MetadataValueFilter{ MetadataValueFilter: &v2.MetadataValueFilter{ Value: []byte(base64.StdEncoding.EncodeToString([]byte(metaValue))), Method: filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS, }, }, } }