From 8df402fb4f2c984581a06e23bb5515f926991717 Mon Sep 17 00:00:00 2001 From: "Marco A." Date: Mon, 1 Sep 2025 18:12:36 +0200 Subject: [PATCH] feat: List users by metadata (#10415) # Which Problems Are Solved Some users have reported the need of retrieving users given a metadata key, metadata value or both. This change introduces metadata search filter on the `ListUsers()` endpoint to allow Zitadel users to search for user records by metadata. The changes affect only v2 APIs. # How the Problems Are Solved - Add new search filter to `ListUserRequest`: `MetaKey` and `MetaValue` - Add SQL indices on metadata key and metadata value - Update query to left join `user_metadata` table # Additional Context - Closes #9053 - Depends on https://github.com/zitadel/zitadel/pull/10567 --------- Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> --- internal/api/grpc/filter/v2/converter.go | 11 + internal/api/grpc/filter/v2/converter_test.go | 164 +++++ .../v2beta/integration_test/query_test.go | 25 +- .../limits_auditlogretention_test.go | 4 +- .../user/v2/integration_test/query_test.go | 159 +++-- .../v2beta/integration_test/query_test.go | 580 +++++++++--------- .../user/v2beta/integration_test/user_test.go | 8 +- internal/integration/name.go | 4 + internal/query/projection/user_metadata.go | 2 + internal/query/user.go | 233 ++++--- internal/query/user_test.go | 10 +- load-test/Makefile | 15 + load-test/package-lock.json | 11 +- load-test/package.json | 2 +- load-test/src/login_ui.ts | 5 +- load-test/src/membership.ts | 3 +- load-test/src/metadata.ts | 4 + load-test/src/oidc.ts | 8 +- .../src/use_cases/users_by_metadata_key.ts | 65 ++ .../src/use_cases/users_by_metadata_value.ts | 67 ++ load-test/src/user.ts | 68 ++ load-test/src/user_grant.ts | 46 ++ proto/zitadel/user/v2beta/user_service.proto | 3 +- 23 files changed, 1019 insertions(+), 478 deletions(-) create mode 100644 internal/api/grpc/filter/v2/converter_test.go create mode 100644 load-test/src/metadata.ts create mode 100644 load-test/src/use_cases/users_by_metadata_key.ts create mode 100644 load-test/src/use_cases/users_by_metadata_value.ts create mode 100644 load-test/src/user_grant.ts diff --git a/internal/api/grpc/filter/v2/converter.go b/internal/api/grpc/filter/v2/converter.go index 98e8bdd4f..533f66075 100644 --- a/internal/api/grpc/filter/v2/converter.go +++ b/internal/api/grpc/filter/v2/converter.go @@ -49,6 +49,17 @@ func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.Timesta } } +func ByteMethodPbToQuery(method filter.ByteFilterMethod) query.BytesComparison { + switch method { + case filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS: + return query.BytesEquals + case filter.ByteFilterMethod_BYTE_FILTER_METHOD_NOT_EQUALS: + return query.BytesNotEquals + default: + return -1 + } +} + func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) { limit = defaults.DefaultQueryLimit if query == nil { diff --git a/internal/api/grpc/filter/v2/converter_test.go b/internal/api/grpc/filter/v2/converter_test.go new file mode 100644 index 000000000..42bebc1ed --- /dev/null +++ b/internal/api/grpc/filter/v2/converter_test.go @@ -0,0 +1,164 @@ +package filter + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +func TestTextMethodPbToQuery(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + input filter.TextFilterMethod + output query.TextComparison + }{ + {"Equals", filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, query.TextEquals}, + {"EqualsIgnoreCase", filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE, query.TextEqualsIgnoreCase}, + {"StartsWith", filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH, query.TextStartsWith}, + {"StartsWithIgnoreCase", filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE, query.TextStartsWithIgnoreCase}, + {"Contains", filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS, query.TextContains}, + {"ContainsIgnoreCase", filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE, query.TextContainsIgnoreCase}, + {"EndsWith", filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH, query.TextEndsWith}, + {"EndsWithIgnoreCase", filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE, query.TextEndsWithIgnoreCase}, + {"Unknown", filter.TextFilterMethod(999), -1}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := TextMethodPbToQuery(tc.input) + + assert.Equal(t, tc.output, got) + }) + } +} + +func TestTimestampMethodPbToQuery(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + input filter.TimestampFilterMethod + output query.TimestampComparison + }{ + {"Equals", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS, query.TimestampEquals}, + {"Before", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE, query.TimestampLess}, + {"After", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER, query.TimestampGreater}, + {"BeforeOrEquals", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS, query.TimestampLessOrEquals}, + {"AfterOrEquals", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, query.TimestampGreaterOrEquals}, + {"Unknown", filter.TimestampFilterMethod(999), -1}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := TimestampMethodPbToQuery(tc.input) + + assert.Equal(t, tc.output, got) + }) + } +} + +func TestByteMethodPbToQuery(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + input filter.ByteFilterMethod + output query.BytesComparison + }{ + {"Equals", filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS, query.BytesEquals}, + {"NotEquals", filter.ByteFilterMethod_BYTE_FILTER_METHOD_NOT_EQUALS, query.BytesNotEquals}, + {"Unknown", filter.ByteFilterMethod(999), -1}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := ByteMethodPbToQuery(tc.input) + + assert.Equal(t, tc.output, got) + }) + } +} + +func TestPaginationPbToQuery(t *testing.T) { + t.Parallel() + + defaults := systemdefaults.SystemDefaults{ + DefaultQueryLimit: 10, + MaxQueryLimit: 100, + } + tt := []struct { + name string + query *filter.PaginationRequest + wantOff uint64 + wantLim uint64 + wantAsc bool + wantErr bool + }{ + { + name: "nil query", + query: nil, + wantOff: 0, + wantLim: 10, + wantAsc: false, + wantErr: false, + }, + { + name: "limit not set", + query: &filter.PaginationRequest{Offset: 5, Limit: 0, Asc: true}, + wantOff: 5, + wantLim: 10, + wantAsc: true, + wantErr: false, + }, + { + name: "limit set below max", + query: &filter.PaginationRequest{Offset: 2, Limit: 50, Asc: false}, + wantOff: 2, + wantLim: 50, + wantAsc: false, + wantErr: false, + }, + { + name: "limit exceeds max", + query: &filter.PaginationRequest{Offset: 1, Limit: 101, Asc: true}, + wantOff: 0, + wantLim: 0, + wantAsc: false, + wantErr: true, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + off, lim, asc, err := PaginationPbToQuery(defaults, tc.query) + + require.Equal(t, tc.wantErr, err != nil) + + assert.Equal(t, tc.wantOff, off) + assert.Equal(t, tc.wantLim, lim) + assert.Equal(t, tc.wantAsc, asc) + }) + } +} + +func TestQueryToPaginationPb(t *testing.T) { + t.Parallel() + + req := query.SearchRequest{Limit: 20} + resp := query.SearchResponse{Count: 123} + got := QueryToPaginationPb(req, resp) + + assert.Equal(t, req.Limit, got.AppliedLimit) + assert.Equal(t, resp.Count, got.TotalResult) +} diff --git a/internal/api/grpc/settings/v2beta/integration_test/query_test.go b/internal/api/grpc/settings/v2beta/integration_test/query_test.go index 3663ee323..24e16ea4f 100644 --- a/internal/api/grpc/settings/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/settings/v2beta/integration_test/query_test.go @@ -7,13 +7,12 @@ import ( "testing" "time" - "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" - "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" ) func TestServer_ListOrganizationSettings(t *testing.T) { @@ -36,7 +35,7 @@ func TestServer_ListOrganizationSettings(t *testing.T) { args: args{ ctx: CTX, dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { - orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp.GetOrganizationId(), true) request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ @@ -56,7 +55,7 @@ func TestServer_ListOrganizationSettings(t *testing.T) { args: args{ ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { - orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp.GetOrganizationId(), true) request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ @@ -82,7 +81,7 @@ func TestServer_ListOrganizationSettings(t *testing.T) { args: args{ ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { - orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp.GetOrganizationId(), true) request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ @@ -131,7 +130,7 @@ func TestServer_ListOrganizationSettings(t *testing.T) { args: args{ ctx: iamOwnerCtx, dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { - orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) settingsResp := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp.GetOrganizationId(), true) request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ @@ -163,11 +162,11 @@ func TestServer_ListOrganizationSettings(t *testing.T) { args: args{ ctx: iamOwnerCtx, dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { - orgResp1 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp1 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) settingsResp1 := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp1.GetOrganizationId(), true) - orgResp2 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp2 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) settingsResp2 := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp2.GetOrganizationId(), true) - orgResp3 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp3 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) settingsResp3 := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp3.GetOrganizationId(), true) request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ @@ -211,11 +210,11 @@ func TestServer_ListOrganizationSettings(t *testing.T) { args: args{ ctx: iamOwnerCtx, dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { - orgResp1 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp1 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp1.GetOrganizationId(), false) - orgResp2 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp2 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) settingsResp2 := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp2.GetOrganizationId(), true) - orgResp3 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), gofakeit.Email()) + orgResp3 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp3.GetOrganizationId(), false) request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ diff --git a/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go b/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go index 981ba13fb..ff7ead43c 100644 --- a/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go +++ b/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go @@ -23,7 +23,7 @@ import ( func TestServer_Limits_AuditLogRetention(t *testing.T) { isoInstance := integration.NewInstance(CTX) - iamOwnerCtx := isoInstance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + iamOwnerCtx := isoInstance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t, isoInstance.Client) beforeTime := time.Now() farPast := timestamppb.New(beforeTime.Add(-10 * time.Hour).UTC()) @@ -37,7 +37,7 @@ func TestServer_Limits_AuditLogRetention(t *testing.T) { }, "wait for added event assertions to pass") _, err := integration.SystemClient().SetLimits(CTX, &system.SetLimitsRequest{ InstanceId: isoInstance.ID(), - AuditLogRetention: durationpb.New(time.Now().Sub(beforeTime)), + AuditLogRetention: durationpb.New(time.Since(beforeTime)), }) require.NoError(t, err) var limitedCounts *eventCounts 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 ccd662a71..6b84239a1 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" + "encoding/base64" "errors" "slices" "testing" @@ -17,6 +18,8 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + "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" @@ -413,7 +416,7 @@ func createUsers(ctx context.Context, orgID string, count int, passwordChangeReq } func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr { - username := gofakeit.Email() + username := integration.Email() return createUserWithUserName(ctx, username, orgID, passwordChangeRequired) } @@ -432,10 +435,10 @@ func createUserWithUserName(ctx context.Context, username string, orgID string, } func TestServer_ListUsers(t *testing.T) { - defer func() { + t.Cleanup(func() { _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) require.NoError(t, err) - }() + }) orgResp := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), gofakeit.Email()) type args struct { @@ -443,7 +446,7 @@ func TestServer_ListUsers(t *testing.T) { req *user.ListUsersRequest dep func(ctx context.Context, request *user.ListUsersRequest) userAttrs } - tests := []struct { + tt := []struct { name string args args want *user.ListUsersResponse @@ -622,7 +625,7 @@ func TestServer_ListUsers(t *testing.T) { }, }, { - name: "list user by id multiple, ok", + name: "list user by id and meta key multiple, ok", args: args{ IamCTX, &user.ListUsersRequest{}, @@ -631,6 +634,13 @@ func TestServer_ListUsers(t *testing.T) { 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 }, }, @@ -639,7 +649,7 @@ func TestServer_ListUsers(t *testing.T) { TotalResult: 3, Timestamp: timestamppb.Now(), }, - SortingColumn: 0, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, Result: []*user.User{ { State: user.UserState_USER_STATE_ACTIVE, @@ -717,6 +727,8 @@ func TestServer_ListUsers(t *testing.T) { 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} }, }, @@ -725,7 +737,7 @@ func TestServer_ListUsers(t *testing.T) { TotalResult: 1, Timestamp: timestamppb.Now(), }, - SortingColumn: 0, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, Result: []*user.User{ { State: user.UserState_USER_STATE_ACTIVE, @@ -761,6 +773,7 @@ func TestServer_ListUsers(t *testing.T) { 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} }, }, @@ -769,7 +782,7 @@ func TestServer_ListUsers(t *testing.T) { TotalResult: 1, Timestamp: timestamppb.Now(), }, - SortingColumn: 0, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, Result: []*user.User{ { State: user.UserState_USER_STATE_ACTIVE, @@ -796,7 +809,7 @@ func TestServer_ListUsers(t *testing.T) { }, }, { - name: "list user in emails multiple, ok", + name: "list user by emails and meta value multiple, ok", args: args{ IamCTX, &user.ListUsersRequest{}, @@ -805,6 +818,15 @@ func TestServer_ListUsers(t *testing.T) { 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 }, }, @@ -813,7 +835,7 @@ func TestServer_ListUsers(t *testing.T) { TotalResult: 3, Timestamp: timestamppb.Now(), }, - SortingColumn: 0, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, Result: []*user.User{ { State: user.UserState_USER_STATE_ACTIVE, @@ -981,6 +1003,7 @@ func TestServer_ListUsers(t *testing.T) { 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 }, }, @@ -989,7 +1012,7 @@ func TestServer_ListUsers(t *testing.T) { TotalResult: 3, Timestamp: timestamppb.Now(), }, - SortingColumn: 0, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, Result: []*user.User{ { State: user.UserState_USER_STATE_ACTIVE, @@ -1065,6 +1088,7 @@ func TestServer_ListUsers(t *testing.T) { info := createUser(ctx, 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, {}} }, }, @@ -1073,7 +1097,7 @@ func TestServer_ListUsers(t *testing.T) { TotalResult: 2, Timestamp: timestamppb.Now(), }, - SortingColumn: 0, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, Result: []*user.User{ { State: user.UserState_USER_STATE_ACTIVE, @@ -1109,7 +1133,6 @@ func TestServer_ListUsers(t *testing.T) { func(ctx context.Context, request *user.ListUsersRequest) userAttrs { orgRespForOrgTests := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), gofakeit.Email()) orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), 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)) @@ -1128,54 +1151,77 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, + { + 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, 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 _, f := range permissionCheckV2Settings { - f := f - for _, tt := range tests { - t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) { + for _, tc := range tt { + t.Run(f.TestNamePrependString+tc.name, func(t1 *testing.T) { setPermissionCheckV2Flag(t, f.SetFlag) - infos := tt.args.dep(IamCTX, tt.args.req) + infos := tc.args.dep(IamCTX, tc.args.req) - 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 { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.args.ctx, 20*time.Second) + 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, tt.want.Result, len(infos)) - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - tt.want.Details.TotalResult = got.Details.TotalResult + 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 tt.want.Result[i] == nil { + if tc.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 { + 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 tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { + if tc.want.Result[i].GetHuman().GetPasswordChanged() != nil { human.PasswordChanged = infos[i].Changed } } - tt.want.Result[i].Details = infos[i].Details + tc.want.Result[i].Details = infos[i].Details } - for i := range tt.want.Result { - if tt.want.Result[i] == nil { + for i := range tc.want.Result { + if tc.want.Result[i] == nil { continue } - assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) + assert.EqualExportedValues(ttt, got.Result[i], tc.want.Result[i]) } } - integration.AssertListDetails(ttt, tt.want, got) + integration.AssertListDetails(ttt, tc.want, got) }, retryDuration, tick, "timeout waiting for expected user result") }) } @@ -1322,3 +1368,44 @@ func OrganizationIdQuery(resourceowner string) *user.SearchQuery { }, } } + +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, + }, + }, + } +} 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 e1b2d27f0..25acb9ed5 100644 --- a/internal/api/grpc/user/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/query_test.go @@ -5,6 +5,7 @@ package user_test import ( "context" "errors" + "fmt" "slices" "testing" "time" @@ -20,7 +21,7 @@ import ( "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" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func detailsV2ToV2beta(obj *object.Details) *object_v2beta.Details { @@ -84,23 +85,23 @@ func TestServer_GetUserByID(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), gofakeit.Email()) type args struct { ctx context.Context - req *user.GetUserByIDRequest - dep func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr + req *user_v2beta.GetUserByIDRequest + dep func(ctx context.Context, request *user_v2beta.GetUserByIDRequest) *userAttr } tests := []struct { name string args args - want *user.GetUserByIDResponse + want *user_v2beta.GetUserByIDResponse wantErr bool }{ { name: "user by ID, no id provided", args: args{ IamCTX, - &user.GetUserByIDRequest{ + &user_v2beta.GetUserByIDRequest{ UserId: "", }, - func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr { + func(ctx context.Context, request *user_v2beta.GetUserByIDRequest) *userAttr { return nil }, }, @@ -110,10 +111,10 @@ func TestServer_GetUserByID(t *testing.T) { name: "user by ID, not found", args: args{ IamCTX, - &user.GetUserByIDRequest{ + &user_v2beta.GetUserByIDRequest{ UserId: "unknown", }, - func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr { + func(ctx context.Context, request *user_v2beta.GetUserByIDRequest) *userAttr { return nil }, }, @@ -123,34 +124,34 @@ func TestServer_GetUserByID(t *testing.T) { name: "user by ID, ok", args: args{ IamCTX, - &user.GetUserByIDRequest{}, - func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr { + &user_v2beta.GetUserByIDRequest{}, + func(ctx context.Context, request *user_v2beta.GetUserByIDRequest) *userAttr { info := createUser(ctx, orgResp.OrganizationId, false) request.UserId = info.UserID return &info }, }, - want: &user.GetUserByIDResponse{ - User: &user.User{ - State: user.UserState_USER_STATE_ACTIVE, + want: &user_v2beta.GetUserByIDResponse{ + User: &user_v2beta.User{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, Username: "", LoginNames: nil, PreferredLoginName: "", - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), AvatarUrl: "", }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -166,34 +167,34 @@ func TestServer_GetUserByID(t *testing.T) { name: "user by ID, passwordChangeRequired, ok", args: args{ IamCTX, - &user.GetUserByIDRequest{}, - func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr { + &user_v2beta.GetUserByIDRequest{}, + func(ctx context.Context, request *user_v2beta.GetUserByIDRequest) *userAttr { info := createUser(ctx, orgResp.OrganizationId, true) request.UserId = info.UserID return &info }, }, - want: &user.GetUserByIDResponse{ - User: &user.User{ - State: user.UserState_USER_STATE_ACTIVE, + want: &user_v2beta.GetUserByIDResponse{ + User: &user_v2beta.User{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, Username: "", LoginNames: nil, PreferredLoginName: "", - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), AvatarUrl: "", }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, PasswordChangeRequired: true, @@ -249,43 +250,43 @@ func TestServer_GetUserByID_Permission(t *testing.T) { newUserID := newOrg.CreatedAdmins[0].GetUserId() type args struct { ctx context.Context - req *user.GetUserByIDRequest + req *user_v2beta.GetUserByIDRequest } tests := []struct { name string args args - want *user.GetUserByIDResponse + want *user_v2beta.GetUserByIDResponse wantErr bool }{ { name: "System, ok", args: args{ SystemCTX, - &user.GetUserByIDRequest{ + &user_v2beta.GetUserByIDRequest{ UserId: newUserID, }, }, - want: &user.GetUserByIDResponse{ - User: &user.User{ - State: user.UserState_USER_STATE_ACTIVE, + want: &user_v2beta.GetUserByIDResponse{ + User: &user_v2beta.User{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, Username: "", LoginNames: nil, PreferredLoginName: "", - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "firstname", FamilyName: "lastname", NickName: gu.Ptr(""), DisplayName: gu.Ptr("firstname lastname"), PreferredLanguage: gu.Ptr("und"), - Gender: user.Gender_GENDER_UNSPECIFIED.Enum(), + Gender: user_v2beta.Gender_GENDER_UNSPECIFIED.Enum(), AvatarUrl: "", }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ Email: newOrgOwnerEmail, }, - Phone: &user.HumanPhone{}, + Phone: &user_v2beta.HumanPhone{}, }, }, }, @@ -299,31 +300,31 @@ func TestServer_GetUserByID_Permission(t *testing.T) { name: "Instance, ok", args: args{ IamCTX, - &user.GetUserByIDRequest{ + &user_v2beta.GetUserByIDRequest{ UserId: newUserID, }, }, - want: &user.GetUserByIDResponse{ - User: &user.User{ - State: user.UserState_USER_STATE_ACTIVE, + want: &user_v2beta.GetUserByIDResponse{ + User: &user_v2beta.User{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, Username: "", LoginNames: nil, PreferredLoginName: "", - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "firstname", FamilyName: "lastname", NickName: gu.Ptr(""), DisplayName: gu.Ptr("firstname lastname"), PreferredLanguage: gu.Ptr("und"), - Gender: user.Gender_GENDER_UNSPECIFIED.Enum(), + Gender: user_v2beta.Gender_GENDER_UNSPECIFIED.Enum(), AvatarUrl: "", }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ Email: newOrgOwnerEmail, }, - Phone: &user.HumanPhone{}, + Phone: &user_v2beta.HumanPhone{}, }, }, }, @@ -337,7 +338,7 @@ func TestServer_GetUserByID_Permission(t *testing.T) { name: "Org, error", args: args{ CTX, - &user.GetUserByIDRequest{ + &user_v2beta.GetUserByIDRequest{ UserId: newUserID, }, }, @@ -347,7 +348,7 @@ func TestServer_GetUserByID_Permission(t *testing.T) { name: "User, error", args: args{ UserCTX, - &user.GetUserByIDRequest{ + &user_v2beta.GetUserByIDRequest{ UserId: newUserID, }, }, @@ -419,7 +420,7 @@ func createUsers(ctx context.Context, orgID string, count int, passwordChangeReq } func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr { - username := gofakeit.Email() + username := integration.Email() // used as default country prefix phone := "+41" + gofakeit.Phone() resp := Instance.CreateHumanUserVerified(ctx, orgID, username, phone) @@ -432,41 +433,41 @@ func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) } func TestServer_ListUsers(t *testing.T) { - defer func() { + t.Cleanup(func() { _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) require.NoError(t, err) - }() + }) orgResp := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), gofakeit.Email()) type args struct { ctx context.Context - req *user.ListUsersRequest - dep func(ctx context.Context, request *user.ListUsersRequest) userAttrs + req *user_v2beta.ListUsersRequest + dep func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs } - tests := []struct { + tt := []struct { name string args args - want *user.ListUsersResponse + want *user_v2beta.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 { + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 0, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{}, + Result: []*user_v2beta.User{}, }, }, { @@ -492,35 +493,35 @@ func TestServer_ListUsers(t *testing.T) { HumanCTX := integration.WithAuthorizationToken(IamCTX, createResp.GetSessionToken()) return args{ HumanCTX, - &user.ListUsersRequest{}, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { return []userAttr{info} }, } }(), - want: &user.ListUsersResponse{ // human user should return itself when calling ListUsers() even if it has no permissions + want: &user_v2beta.ListUsersResponse{ // human user should return itself when calling ListUsers() even if it has no permissions Details: &object_v2beta.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, PasswordChangeRequired: true, @@ -535,38 +536,38 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id, ok", args: args{ IamCTX, - &user.ListUsersRequest{}, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) - request.Queries = []*user.SearchQuery{} + request.Queries = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -579,38 +580,38 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id, passwordChangeRequired, ok", args: args{ IamCTX, - &user.ListUsersRequest{}, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, true) - request.Queries = []*user.SearchQuery{} + request.Queries = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, PasswordChangeRequired: true, @@ -625,80 +626,80 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id multiple, ok", args: args{ IamCTX, - &user.ListUsersRequest{}, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { infos := createUsers(ctx, orgResp.OrganizationId, 3, false) - request.Queries = []*user.SearchQuery{} + request.Queries = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs())) return infos }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, }, }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, }, }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -711,38 +712,38 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by username, ok", args: args{ IamCTX, - &user.ListUsersRequest{}, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) - request.Queries = []*user.SearchQuery{} + request.Queries = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, UsernameQuery(info.Username)) return []userAttr{info} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -755,41 +756,41 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ + &user_v2beta.ListUsersRequest{ + Queries: []*user_v2beta.SearchQuery{ OrganizationIdQuery(orgResp.OrganizationId), }, }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) - request.Queries = []*user.SearchQuery{} + request.Queries = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username})) return []userAttr{info} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -802,78 +803,78 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails multiple, ok", args: args{ IamCTX, - &user.ListUsersRequest{}, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { infos := createUsers(ctx, orgResp.OrganizationId, 3, false) - request.Queries = []*user.SearchQuery{} + request.Queries = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) return infos }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, }, }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, }, }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -886,61 +887,61 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails no found, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ + &user_v2beta.ListUsersRequest{ + Queries: []*user_v2beta.SearchQuery{ OrganizationIdQuery(orgResp.OrganizationId), InUserEmailsQuery([]string{"notfound"}), }, }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { return []userAttr{} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 0, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{}, + Result: []*user_v2beta.User{}, }, }, { name: "list user phone, ok", args: args{ IamCTX, - &user.ListUsersRequest{}, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) - request.Queries = []*user.SearchQuery{} + request.Queries = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, PhoneQuery(info.Phone)) return []userAttr{info} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 1, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -953,103 +954,103 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails no found, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ + &user_v2beta.ListUsersRequest{ + Queries: []*user_v2beta.SearchQuery{ OrganizationIdQuery(orgResp.OrganizationId), InUserEmailsQuery([]string{"notfound"}), }, }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + func(ctx context.Context, request *user_v2beta.ListUsersRequest) userAttrs { return []userAttr{} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 0, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{}, + Result: []*user_v2beta.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(), gofakeit.Email()) + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.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 = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) return infos }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 3, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, }, }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, }, }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -1062,38 +1063,38 @@ 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, integration.OrganizationName(), gofakeit.Email()) + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.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 = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId)) return []userAttr{info, {}} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 2, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ + State: user_v2beta.UserState_USER_STATE_ACTIVE, + Type: &user_v2beta.User_Human{ + Human: &user_v2beta.HumanUser{ + Profile: &user_v2beta.HumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), DisplayName: gu.Ptr("Mickey Mouse"), PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), + Gender: user_v2beta.Gender_GENDER_MALE.Enum(), }, - Email: &user.HumanEmail{ + Email: &user_v2beta.HumanEmail{ IsVerified: true, }, - Phone: &user.HumanPhone{ + Phone: &user_v2beta.HumanPhone{ IsVerified: true, }, }, @@ -1108,24 +1109,24 @@ func TestServer_ListUsers(t *testing.T) { 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(), gofakeit.Email()) - orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), gofakeit.Email()) + &user_v2beta.ListUsersRequest{}, + func(ctx context.Context, request *user_v2beta.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 = []*user_v2beta.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests2.OrganizationId)) return []userAttr{{}} }, }, - want: &user.ListUsersResponse{ + want: &user_v2beta.ListUsersResponse{ Details: &object_v2beta.ListDetails{ TotalResult: 0, Timestamp: timestamppb.Now(), }, SortingColumn: 0, - Result: []*user.User{ + Result: []*user_v2beta.User{ // this is the admin of the org craated in Instance.CreateOrganization() nil, }, @@ -1133,105 +1134,104 @@ func TestServer_ListUsers(t *testing.T) { }, } 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) + for _, tc := range tt { + t.Run(f.TestNamePrependString+tc.name, func(t1 *testing.T) { + setPermissionCheckV2Flag(t1, f.SetFlag) + infos := tc.args.dep(IamCTX, tc.args.req) // 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 { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.args.ctx, 20*time.Second) + 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, tt.want.Result, len(infos)) + require.Len(ttt, tc.want.Result, len(infos)) // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { + if assert.Len(ttt, got.Result, len(tc.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 + tc.want.Details.TotalResult = got.Details.TotalResult // fill in userid and username as it is generated for i := range infos { - if tt.want.Result[i] == nil { + if tc.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 { + 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 tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { + if tc.want.Result[i].GetHuman().GetPasswordChanged() != nil { human.PasswordChanged = infos[i].Changed } } - tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details) + tc.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details) } - for i := range tt.want.Result { - if tt.want.Result[i] == nil { + for i := range tc.want.Result { + if tc.want.Result[i] == nil { continue } - assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) + assert.EqualExportedValues(ttt, got.Result[i], tc.want.Result[i]) } } - integration.AssertListDetails(ttt, tt.want, got) + integration.AssertListDetails(ttt, tc.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{ +func InUserIDsQuery(ids []string) *user_v2beta.SearchQuery { + return &user_v2beta.SearchQuery{ + Query: &user_v2beta.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user_v2beta.InUserIDQuery{ UserIds: ids, }, }, } } -func InUserEmailsQuery(emails []string) *user.SearchQuery { - return &user.SearchQuery{ - Query: &user.SearchQuery_InUserEmailsQuery{ - InUserEmailsQuery: &user.InUserEmailsQuery{ +func InUserEmailsQuery(emails []string) *user_v2beta.SearchQuery { + return &user_v2beta.SearchQuery{ + Query: &user_v2beta.SearchQuery_InUserEmailsQuery{ + InUserEmailsQuery: &user_v2beta.InUserEmailsQuery{ UserEmails: emails, }, }, } } -func PhoneQuery(number string) *user.SearchQuery { - return &user.SearchQuery{ - Query: &user.SearchQuery_PhoneQuery{ - PhoneQuery: &user.PhoneQuery{ +func PhoneQuery(number string) *user_v2beta.SearchQuery { + return &user_v2beta.SearchQuery{ + Query: &user_v2beta.SearchQuery_PhoneQuery{ + PhoneQuery: &user_v2beta.PhoneQuery{ Number: number, }, }, } } -func UsernameQuery(username string) *user.SearchQuery { - return &user.SearchQuery{ - Query: &user.SearchQuery_UserNameQuery{ - UserNameQuery: &user.UserNameQuery{ +func UsernameQuery(username string) *user_v2beta.SearchQuery { + return &user_v2beta.SearchQuery{ + Query: &user_v2beta.SearchQuery_UserNameQuery{ + UserNameQuery: &user_v2beta.UserNameQuery{ UserName: username, }, }, } } -func OrganizationIdQuery(resourceowner string) *user.SearchQuery { - return &user.SearchQuery{ - Query: &user.SearchQuery_OrganizationIdQuery{ - OrganizationIdQuery: &user.OrganizationIdQuery{ +func OrganizationIdQuery(resourceowner string) *user_v2beta.SearchQuery { + return &user_v2beta.SearchQuery{ + Query: &user_v2beta.SearchQuery_OrganizationIdQuery{ + OrganizationIdQuery: &user_v2beta.OrganizationIdQuery{ OrganizationId: resourceowner, }, }, diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index 6bae84c24..36d410e9a 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -45,11 +45,11 @@ func TestMain(m *testing.M) { Instance = integration.NewInstance(ctx) - UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission) - IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) - LoginCTX = Instance.WithAuthorization(ctx, integration.UserTypeLogin) + UserCTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeNoPermission) + IamCTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeIAMOwner) + LoginCTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeLogin) SystemCTX = integration.WithSystemAuthorization(ctx) - CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) + CTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeOrgOwner) Client = Instance.Client.UserV2beta return m.Run() }()) diff --git a/internal/integration/name.go b/internal/integration/name.go index 443ae7a3a..4a7a14fcd 100644 --- a/internal/integration/name.go +++ b/internal/integration/name.go @@ -11,6 +11,10 @@ func OrganizationName() string { return company() } +func Email() string { + return RandString(5) + gofakeit.Email() +} + // appName private function to add a random string to the gofakeit.AppName function func appName() string { return gofakeit.AppName() + "-" + RandString(5) diff --git a/internal/query/projection/user_metadata.go b/internal/query/projection/user_metadata.go index 6de98ad73..3edf30280 100644 --- a/internal/query/projection/user_metadata.go +++ b/internal/query/projection/user_metadata.go @@ -49,6 +49,8 @@ func (*userMetadataProjection) Init() *old_handler.Check { }, handler.NewPrimaryKey(UserMetadataColumnInstanceID, UserMetadataColumnUserID, UserMetadataColumnKey), handler.WithIndex(handler.NewIndex("resource_owner", []string{UserGrantResourceOwner})), + handler.WithIndex(handler.NewIndex("metadata_key", []string{UserMetadataColumnKey})), + handler.WithIndex(handler.NewIndex("metadata_value", []string{UserMetadataColumnValue})), ), ) } diff --git a/internal/query/user.go b/internal/query/user.go index 66c7abb22..0dd129e18 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -863,27 +863,7 @@ func scanUser(row *sql.Row) (*User, error) { var count int preferredLoginName := sql.NullString{} - humanID := sql.NullString{} - firstName := sql.NullString{} - lastName := sql.NullString{} - nickName := sql.NullString{} - displayName := sql.NullString{} - preferredLanguage := sql.NullString{} - gender := sql.NullInt32{} - avatarKey := sql.NullString{} - email := sql.NullString{} - isEmailVerified := sql.NullBool{} - phone := sql.NullString{} - isPhoneVerified := sql.NullBool{} - passwordChangeRequired := sql.NullBool{} - passwordChanged := sql.NullTime{} - mfaInitSkipped := sql.NullTime{} - - machineID := sql.NullString{} - name := sql.NullString{} - description := sql.NullString{} - encodedHash := sql.NullString{} - accessTokenType := sql.NullInt32{} + human, machine := sqlHuman{}, sqlMachine{} err := row.Scan( &u.ID, @@ -896,26 +876,26 @@ func scanUser(row *sql.Row) (*User, error) { &u.Username, &u.LoginNames, &preferredLoginName, - &humanID, - &firstName, - &lastName, - &nickName, - &displayName, - &preferredLanguage, - &gender, - &avatarKey, - &email, - &isEmailVerified, - &phone, - &isPhoneVerified, - &passwordChangeRequired, - &passwordChanged, - &mfaInitSkipped, - &machineID, - &name, - &description, - &encodedHash, - &accessTokenType, + &human.humanID, + &human.firstName, + &human.lastName, + &human.nickName, + &human.displayName, + &human.preferredLanguage, + &human.gender, + &human.avatarKey, + &human.email, + &human.isEmailVerified, + &human.phone, + &human.isPhoneVerified, + &human.passwordChangeRequired, + &human.passwordChanged, + &human.mfaInitSkipped, + &machine.machineID, + &machine.name, + &machine.description, + &machine.encodedSecret, + &machine.accessTokenType, &count, ) @@ -928,29 +908,29 @@ func scanUser(row *sql.Row) (*User, error) { u.PreferredLoginName = preferredLoginName.String - if humanID.Valid { + if human.humanID.Valid { u.Human = &Human{ - FirstName: firstName.String, - LastName: lastName.String, - NickName: nickName.String, - DisplayName: displayName.String, - AvatarKey: avatarKey.String, - PreferredLanguage: language.Make(preferredLanguage.String), - Gender: domain.Gender(gender.Int32), - Email: domain.EmailAddress(email.String), - IsEmailVerified: isEmailVerified.Bool, - Phone: domain.PhoneNumber(phone.String), - IsPhoneVerified: isPhoneVerified.Bool, - PasswordChangeRequired: passwordChangeRequired.Bool, - PasswordChanged: passwordChanged.Time, - MFAInitSkipped: mfaInitSkipped.Time, + FirstName: human.firstName.String, + LastName: human.lastName.String, + NickName: human.nickName.String, + DisplayName: human.displayName.String, + AvatarKey: human.avatarKey.String, + PreferredLanguage: language.Make(human.preferredLanguage.String), + Gender: domain.Gender(human.gender.Int32), + Email: domain.EmailAddress(human.email.String), + IsEmailVerified: human.isEmailVerified.Bool, + Phone: domain.PhoneNumber(human.phone.String), + IsPhoneVerified: human.isPhoneVerified.Bool, + PasswordChangeRequired: human.passwordChangeRequired.Bool, + PasswordChanged: human.passwordChanged.Time, + MFAInitSkipped: human.mfaInitSkipped.Time, } - } else if machineID.Valid { + } else if machine.machineID.Valid { u.Machine = &Machine{ - Name: name.String, - Description: description.String, - EncodedSecret: encodedHash.String, - AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32), + Name: machine.name.String, + Description: machine.description.String, + EncodedSecret: machine.encodedSecret.String, + AccessTokenType: domain.OIDCTokenType(machine.accessTokenType.Int32), } } return u, nil @@ -1316,15 +1296,18 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { HumanIsPhoneVerifiedCol.identifier(), HumanPasswordChangeRequiredCol.identifier(), HumanPasswordChangedCol.identifier(), + HumanMFAInitSkippedCol.identifier(), MachineUserIDCol.identifier(), MachineNameCol.identifier(), MachineDescriptionCol.identifier(), MachineSecretCol.identifier(), MachineAccessTokenTypeCol.identifier(), countColumn.identifier()). + Distinct(). From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(MachineUserIDCol, UserIDCol)). + LeftJoin(join(UserMetadataUserIDCol, UserIDCol)). JoinClause(joinLoginNames). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Users, error) { @@ -1335,26 +1318,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { loginNames := database.TextArray[string]{} preferredLoginName := sql.NullString{} - humanID := sql.NullString{} - firstName := sql.NullString{} - lastName := sql.NullString{} - nickName := sql.NullString{} - displayName := sql.NullString{} - preferredLanguage := sql.NullString{} - gender := sql.NullInt32{} - avatarKey := sql.NullString{} - email := sql.NullString{} - isEmailVerified := sql.NullBool{} - phone := sql.NullString{} - isPhoneVerified := sql.NullBool{} - passwordChangeRequired := sql.NullBool{} - passwordChanged := sql.NullTime{} - - machineID := sql.NullString{} - name := sql.NullString{} - description := sql.NullString{} - encodedHash := sql.NullString{} - accessTokenType := sql.NullInt32{} + human, machine := sqlHuman{}, sqlMachine{} err := rows.Scan( &u.ID, @@ -1367,25 +1331,29 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { &u.Username, &loginNames, &preferredLoginName, - &humanID, - &firstName, - &lastName, - &nickName, - &displayName, - &preferredLanguage, - &gender, - &avatarKey, - &email, - &isEmailVerified, - &phone, - &isPhoneVerified, - &passwordChangeRequired, - &passwordChanged, - &machineID, - &name, - &description, - &encodedHash, - &accessTokenType, + + &human.humanID, + &human.firstName, + &human.lastName, + &human.nickName, + &human.displayName, + &human.preferredLanguage, + &human.gender, + &human.avatarKey, + &human.email, + &human.isEmailVerified, + &human.phone, + &human.isPhoneVerified, + &human.passwordChangeRequired, + &human.passwordChanged, + &human.mfaInitSkipped, + + &machine.machineID, + &machine.name, + &machine.description, + &machine.encodedSecret, + &machine.accessTokenType, + &count, ) if err != nil { @@ -1397,28 +1365,29 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { u.PreferredLoginName = preferredLoginName.String } - if humanID.Valid { + if human.humanID.Valid { u.Human = &Human{ - FirstName: firstName.String, - LastName: lastName.String, - NickName: nickName.String, - DisplayName: displayName.String, - AvatarKey: avatarKey.String, - PreferredLanguage: language.Make(preferredLanguage.String), - Gender: domain.Gender(gender.Int32), - Email: domain.EmailAddress(email.String), - IsEmailVerified: isEmailVerified.Bool, - Phone: domain.PhoneNumber(phone.String), - IsPhoneVerified: isPhoneVerified.Bool, - PasswordChangeRequired: passwordChangeRequired.Bool, - PasswordChanged: passwordChanged.Time, + FirstName: human.firstName.String, + LastName: human.lastName.String, + NickName: human.nickName.String, + DisplayName: human.displayName.String, + AvatarKey: human.avatarKey.String, + PreferredLanguage: language.Make(human.preferredLanguage.String), + Gender: domain.Gender(human.gender.Int32), + Email: domain.EmailAddress(human.email.String), + IsEmailVerified: human.isEmailVerified.Bool, + Phone: domain.PhoneNumber(human.phone.String), + IsPhoneVerified: human.isPhoneVerified.Bool, + PasswordChangeRequired: human.passwordChangeRequired.Bool, + PasswordChanged: human.passwordChanged.Time, + MFAInitSkipped: human.mfaInitSkipped.Time, } - } else if machineID.Valid { + } else if machine.machineID.Valid { u.Machine = &Machine{ - Name: name.String, - Description: description.String, - EncodedSecret: encodedHash.String, - AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32), + Name: machine.name.String, + Description: machine.description.String, + EncodedSecret: machine.encodedSecret.String, + AccessTokenType: domain.OIDCTokenType(machine.accessTokenType.Int32), } } @@ -1437,3 +1406,29 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { }, nil } } + +type sqlHuman struct { + humanID sql.NullString + firstName sql.NullString + lastName sql.NullString + nickName sql.NullString + displayName sql.NullString + preferredLanguage sql.NullString + gender sql.NullInt32 + avatarKey sql.NullString + email sql.NullString + isEmailVerified sql.NullBool + phone sql.NullString + isPhoneVerified sql.NullBool + passwordChangeRequired sql.NullBool + passwordChanged sql.NullTime + mfaInitSkipped sql.NullTime +} + +type sqlMachine struct { + machineID sql.NullString + name sql.NullString + description sql.NullString + encodedSecret sql.NullString + accessTokenType sql.NullInt32 +} diff --git a/internal/query/user_test.go b/internal/query/user_test.go index ae5f6be20..4fce261f8 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -366,7 +366,7 @@ var ( "password_set", "count", } - usersQuery = `SELECT projections.users14.id,` + + usersQuery = `SELECT DISTINCT projections.users14.id,` + ` projections.users14.creation_date,` + ` projections.users14.change_date,` + ` projections.users14.resource_owner,` + @@ -390,6 +390,7 @@ var ( ` projections.users14_humans.is_phone_verified,` + ` projections.users14_humans.password_change_required,` + ` projections.users14_humans.password_changed,` + + ` projections.users14_humans.mfa_init_skipped,` + ` projections.users14_machines.user_id,` + ` projections.users14_machines.name,` + ` projections.users14_machines.description,` + @@ -399,6 +400,7 @@ var ( ` FROM projections.users14` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + ` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` + + ` LEFT JOIN projections.user_metadata5 ON projections.users14.id = projections.user_metadata5.user_id AND projections.users14.instance_id = projections.user_metadata5.instance_id` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE` usersCols = []string{ "id", @@ -426,6 +428,7 @@ var ( "is_phone_verified", "password_change_required", "password_changed", + "mfa_init_skipped", // machine "user_id", "name", @@ -992,6 +995,7 @@ func Test_UserPrepares(t *testing.T) { true, true, testNow, + testNow, // machine nil, nil, @@ -1032,6 +1036,7 @@ func Test_UserPrepares(t *testing.T) { IsPhoneVerified: true, PasswordChangeRequired: true, PasswordChanged: testNow, + MFAInitSkipped: testNow, }, }, }, @@ -1071,6 +1076,7 @@ func Test_UserPrepares(t *testing.T) { true, true, testNow, + testNow, // machine nil, nil, @@ -1104,6 +1110,7 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, + nil, // machine "id", "name", @@ -1144,6 +1151,7 @@ func Test_UserPrepares(t *testing.T) { IsPhoneVerified: true, PasswordChangeRequired: true, PasswordChanged: testNow, + MFAInitSkipped: testNow, }, }, { diff --git a/load-test/Makefile b/load-test/Makefile index 5c515c70b..808280009 100644 --- a/load-test/Makefile +++ b/load-test/Makefile @@ -61,6 +61,21 @@ machine_jwt_profile_grant_single_user: ensure_modules ensure_key_pair bundle cd ../../xk6-modules && xk6 build --with xk6-zitadel=. ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_jwt_profile_grant_single_user.js --vus ${VUS} --duration ${DURATION} --out csv=output/machine_jwt_profile_grant_single_user_${DATE}.csv +.PHONY: verify_all_user_grants_exist +verify_all_user_grants_exist: ensure_modules bundle + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/verify_all_user_grants_exist.js --vus ${VUS} --duration ${DURATION} +# --out csv=output/verify_all_user_grants_exist_${DATE}.csv + +.PHONY: users_by_metadata_key +users_by_metadata_key: ensure_modules bundle + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/users_by_metadata_key.js --vus ${VUS} --duration ${DURATION} +# --out csv=output/users_by_metadata_${DATE}.csv + +.PHONY: users_by_metadata_value +users_by_metadata_value: ensure_modules bundle + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/users_by_metadata_value.js --vus ${VUS} --duration ${DURATION} +# --out csv=output/users_by_metadata_${DATE}.csv + .PHONY: lint lint: npm i diff --git a/load-test/package-lock.json b/load-test/package-lock.json index 3ae4e847c..e103b47bd 100644 --- a/load-test/package-lock.json +++ b/load-test/package-lock.json @@ -14,7 +14,7 @@ "@babel/plugin-proposal-object-rest-spread": "7.13.8", "@babel/preset-env": "7.23.8", "@babel/preset-typescript": "7.23.3", - "@types/k6": ">=0.50.0", + "@types/k6": ">=1.2.0", "@types/webpack": "5.28.5", "babel-loader": "9.1.3", "clean-webpack-plugin": "4.0.0", @@ -1880,10 +1880,11 @@ "dev": true }, "node_modules/@types/k6": { - "version": "0.54.1", - "resolved": "https://registry.npmjs.org/@types/k6/-/k6-0.54.1.tgz", - "integrity": "sha512-ezTMhuWr3TZIMOqAv51/4rD5T4FE1pSryrh5BCgxsniuqxbi5jQ3YEiOlO9C1+LvJcliC2byyd2Cw6cnUY7CLg==", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/k6/-/k6-1.2.0.tgz", + "integrity": "sha512-7F/vjekOUCzFi5AY+yYBOL38iEtN9EXufdKOAMFkJsdLryJ3/bB/W9jppED1FO5o7zeaPf8M46ardYPD8xgy/w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/minimatch": { "version": "5.1.2", diff --git a/load-test/package.json b/load-test/package.json index fd47427f6..b57f1770b 100644 --- a/load-test/package.json +++ b/load-test/package.json @@ -13,7 +13,7 @@ "@babel/plugin-proposal-object-rest-spread": "7.13.8", "@babel/preset-env": "7.23.8", "@babel/preset-typescript": "7.23.3", - "@types/k6": ">=0.50.0", + "@types/k6": ">=1.2.0", "@types/webpack": "5.28.5", "babel-loader": "9.1.3", "clean-webpack-plugin": "4.0.0", diff --git a/load-test/src/login_ui.ts b/load-test/src/login_ui.ts index 7ab73e258..eea7d837e 100644 --- a/load-test/src/login_ui.ts +++ b/load-test/src/login_ui.ts @@ -86,7 +86,7 @@ function enterPassword(page: Response, user: User): Response { } check(response, { - 'password status ok': (r) => r.status >= 200 && r.status < 300 || fail('enter password failed'), + 'password status ok': (r) => (r.status >= 200 && r.status < 300) || fail('enter password failed'), 'password callback': (r) => r.url.startsWith(url('/ui/console/auth/callback?code=')) || fail(`wrong password callback: ${r.url}`), }); @@ -117,7 +117,8 @@ function token(code = '') { tokenTrend.add(response.timings.duration); check(response, { - 'token status ok': (r) => r.status >= 200 && r.status < 300 || fail(`invalid token response status: ${r.status} body: ${r.body}`), + 'token status ok': (r) => + (r.status >= 200 && r.status < 300) || fail(`invalid token response status: ${r.status} body: ${r.body}`), }); const token = new Tokens(response.json() as JSONObject); check(token, { diff --git a/load-test/src/membership.ts b/load-test/src/membership.ts index 9e0d0d8e9..9ab0f7902 100644 --- a/load-test/src/membership.ts +++ b/load-test/src/membership.ts @@ -19,7 +19,8 @@ export async function addIAMMember(userId: string, roles: string[], accessToken: }, ); check(res, { - 'member added successful': (r) => r.status >= 200 && r.status < 300 || fail(`unable add member: ${JSON.stringify(res)}`), + 'member added successful': (r) => + (r.status >= 200 && r.status < 300) || fail(`unable add member: ${JSON.stringify(res)}`), }); addIAMMemberTrend.add(res.timings.duration); } diff --git a/load-test/src/metadata.ts b/load-test/src/metadata.ts new file mode 100644 index 000000000..df7f632f7 --- /dev/null +++ b/load-test/src/metadata.ts @@ -0,0 +1,4 @@ +export type Metadata = { + key: string; + value: string; +}; diff --git a/load-test/src/oidc.ts b/load-test/src/oidc.ts index 2bbd3bdab..83dda3c77 100644 --- a/load-test/src/oidc.ts +++ b/load-test/src/oidc.ts @@ -30,7 +30,7 @@ function configuration() { const res = http.get(url('/.well-known/openid-configuration')); check(res, { - 'openid configuration': (r) => r.status >= 200 && r.status < 300 || fail('unable to load openid configuration'), + 'openid configuration': (r) => (r.status >= 200 && r.status < 300) || fail('unable to load openid configuration'), }); oidcConfig = res.json(); @@ -176,7 +176,8 @@ export async function authRequestByID(id: string, tokens: any): Promise r.status >= 200 && r.status < 300 || fail(`auth request by failed: ${JSON.stringify(r)}`), + 'authorize status ok': (r) => + (r.status >= 200 && r.status < 300) || fail(`auth request by failed: ${JSON.stringify(r)}`), }); authRequestByIDTrend.add(response.timings.duration); return response; @@ -202,7 +203,8 @@ export async function finalizeAuthRequest(id: string, session: any, tokens: any) }, ); check(res, { - 'finalize auth request status ok': (r) => r.status >= 200 && r.status < 300 || fail(`finalize auth request failed: ${JSON.stringify(r)}`), + 'finalize auth request status ok': (r) => + (r.status >= 200 && r.status < 300) || fail(`finalize auth request failed: ${JSON.stringify(r)}`), }); finalizeAuthRequestTrend.add(res.timings.duration); diff --git a/load-test/src/use_cases/users_by_metadata_key.ts b/load-test/src/use_cases/users_by_metadata_key.ts new file mode 100644 index 000000000..87adfef65 --- /dev/null +++ b/load-test/src/use_cases/users_by_metadata_key.ts @@ -0,0 +1,65 @@ +import { loginByUsernamePassword } from '../login_ui'; +import { createOrg, removeOrg } from '../org'; +import { createHuman, User, createMachine, setUserMetadata, listUsers } from '../user'; +import { Config } from '../config'; +import { check } from 'k6'; +import encoding from 'k6/encoding'; + +const userAmount = parseInt(__ENV.USER_AMOUNT) || 2500; + +export async function setup() { + const tokens = loginByUsernamePassword(Config.admin as User); + console.info('setup: admin signed in'); + + const org = await createOrg(tokens.accessToken!); + console.info(`setup: org (${org.organizationId}) created`); + + const users: User[] = []; + + await Promise.all( + Array.from({ length: userAmount }, async (_, i) => { + let user: User; + let type: 'human' | 'machine'; + if (i % 2 === 0) { + user = await createHuman(`zitizen-${i}`, org, tokens.accessToken!); + type = 'human'; + } else { + user = await createMachine(`zitachine-${i}`, org, tokens.accessToken!); + type = 'machine'; + } + users.push(user); + await setUserMetadata( + [ + { key: 'type', value: encoding.b64encode(type, 'rawurl') }, + { key: 'org', value: encoding.b64encode(org.organizationId, 'rawurl') }, + { key: 'id', value: encoding.b64encode(user.userId, 'rawurl') }, + ], + user.userId, + tokens.accessToken!, + ); + if (i % 10 === 0) { + console.log(`setup: ${i} of ${userAmount} users setup`); + } + }), + ); + console.info(`setup: ${users.length} users created`); + + return { tokens, org, users }; +} + +export default async function (data: any) { + const result = await listUsers( + { + queries: [{ metadataKeyFilter: { key: 'org', method: 'TEXT_FILTER_METHOD_EQUALS' } }], + }, + data.tokens.accessToken!, + ); + check(result, { + 'total result length': (res) => res.details.totalResult == userAmount, + }) || console.log(`unexpected amount of users. expected ${userAmount} but got ${result.details.totalResult}`); +} + +export function teardown(data: any) { + removeOrg(data.org, data.tokens.accessToken); + console.info('teardown: org removed'); +} diff --git a/load-test/src/use_cases/users_by_metadata_value.ts b/load-test/src/use_cases/users_by_metadata_value.ts new file mode 100644 index 000000000..10b067955 --- /dev/null +++ b/load-test/src/use_cases/users_by_metadata_value.ts @@ -0,0 +1,67 @@ +import { loginByUsernamePassword } from '../login_ui'; +import { createOrg, removeOrg } from '../org'; +import { createHuman, User, createMachine, setUserMetadata, listUsers } from '../user'; +import { Config } from '../config'; +import { check } from 'k6'; +import encoding from 'k6/encoding'; + +const userAmount = parseInt(__ENV.USER_AMOUNT) || 2500; + +export async function setup() { + const tokens = loginByUsernamePassword(Config.admin as User); + console.info('setup: admin signed in'); + + const org = await createOrg(tokens.accessToken!); + console.info(`setup: org (${org.organizationId}) created`); + + const users: User[] = []; + + await Promise.all( + Array.from({ length: userAmount }, async (_, i) => { + let user: User; + let type: 'human' | 'machine'; + if (i % 2 === 0) { + user = await createHuman(`zitizen-${i}`, org, tokens.accessToken!); + type = 'human'; + } else { + user = await createMachine(`zitachine-${i}`, org, tokens.accessToken!); + type = 'machine'; + } + users.push(user); + await setUserMetadata( + [ + { key: 'type', value: encoding.b64encode(type, 'rawurl') }, + { key: 'org', value: encoding.b64encode(org.organizationId, 'rawurl') }, + { key: 'id', value: encoding.b64encode(user.userId, 'rawurl') }, + ], + user.userId, + tokens.accessToken!, + ); + if (i % 10 === 0) { + console.log(`setup: ${i} of ${userAmount} users setup`); + } + }), + ); + console.info(`setup: ${users.length} users created`); + + return { tokens, org, users }; +} + +export default async function (data: any) { + const result = await listUsers( + { + queries: [ + { metadataValueFilter: { value: encoding.b64encode('human', 'rawurl'), method: 'BYTE_FILTER_METHOD_EQUALS' } }, + ], + }, + data.tokens.accessToken!, + ); + check(result, { + 'total result length': (res) => res.details.totalResult == userAmount / 2, + }) || console.log(`unexpected amount of users. expected ${userAmount / 2} but got ${result.details.totalResult}`); +} + +export function teardown(data: any) { + removeOrg(data.org, data.tokens.accessToken); + console.info('teardown: org removed'); +} diff --git a/load-test/src/user.ts b/load-test/src/user.ts index eaff33acf..032e4967a 100644 --- a/load-test/src/user.ts +++ b/load-test/src/user.ts @@ -3,6 +3,7 @@ import { Org } from './org'; import http, { RefinedResponse } from 'k6/http'; import url from './url'; import { check } from 'k6'; +import { Metadata } from './metadata'; export type User = { userId: string; @@ -298,3 +299,70 @@ export function deleteUser(userId: string, org: Org, accessToken: string): Promi }); }); } + +const setUserMetadataTrend = new Trend('set_user_metadata_duration', true); +export function setUserMetadata(metadata: Metadata[], userId: string, accessToken: string): Promise> { + return new Promise((resolve, reject) => { + let response = http.asyncRequest('POST', url(`/v2/users/${userId}/metadata`), JSON.stringify({ metadata: metadata }), { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + response + .then((res) => { + check(res, { + 'set user metadata is status ok': (r) => r.status >= 200 && r.status < 300, + }) || console.log(`unable to set user metadata (user id: ${userId}) status: ${res.status} body: ${res.body}`); + setUserMetadataTrend.add(res.timings.duration); + resolve(res); + }) + .catch((reason) => { + reject(reason); + }); + }); +} + +export type ListUsersRequest = { + queries: { + metadataKeyFilter?: { + key: string; + method: 'TEXT_FILTER_METHOD_EQUALS' | 'TEXT_FILTER_METHOD_CONTAINS' | 'TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE'; + }; + metadataValueFilter?: { + value: string; + method: 'BYTE_FILTER_METHOD_EQUALS' | 'BYTE_FILTER_METHOD_NOT_EQUALS'; + }; + }[]; +}; + +export type ListUsersResult = { + details: { + totalResult: number; + }; +}; + +const listUsersTrend = new Trend('list_users_duration', true); +export function listUsers(body: ListUsersRequest, accessToken: string): Promise { + return new Promise((resolve, reject) => { + let response = http.asyncRequest('POST', url(`/v2/users`), JSON.stringify(body), { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + response + .then((res) => { + check(res, { + 'list users is status ok': (r) => r.status >= 200 && r.status < 300, + }) || console.log(`unable to list users status: ${res.status} body: ${res.body}`); + listUsersTrend.add(res.timings.duration); + resolve(res.json()! as ListUsersResult); + }) + .catch((reason) => { + reject(reason); + }); + }); +} diff --git a/load-test/src/user_grant.ts b/load-test/src/user_grant.ts new file mode 100644 index 000000000..774ea9978 --- /dev/null +++ b/load-test/src/user_grant.ts @@ -0,0 +1,46 @@ +import http from 'k6/http'; +import { Trend } from 'k6/metrics'; +import url from './url'; +import { check, fail } from 'k6'; +import { Project } from './project'; +import { Org } from './org'; + +export type UserGrant = { + userGrantId: string; +}; + +const addUserGrantTrend = new Trend('user_grant_add', true); +export async function addUserGrant( + org: Org, + userId: string, + project: Project, + roles: string[], + accessToken: string, +): Promise { + return new Promise((resolve, reject) => { + let response = http.asyncRequest( + 'POST', + url(`/management/v1/users/${userId}/grants`), + JSON.stringify({ + projectId: project.id, + roleKeys: roles, + }), + { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'x-zitadel-orgid': org.organizationId, + }, + }, + ); + + response.then((res) => { + check(res, { + 'add User Grant status ok': (r) => r.status >= 200 && r.status < 300, + }) || reject(`unable to add User Grant status: ${res.status} body: ${res.body}`); + + addUserGrantTrend.add(res.timings.duration); + resolve(res.json() as UserGrant); + }); + }); +} diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index bcb091abf..183527032 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -1065,6 +1065,7 @@ service UserService { }; }; } + } message AddHumanUserRequest{ @@ -1947,4 +1948,4 @@ enum AuthenticationMethodType { AUTHENTICATION_METHOD_TYPE_U2F = 5; AUTHENTICATION_METHOD_TYPE_OTP_SMS = 6; AUTHENTICATION_METHOD_TYPE_OTP_EMAIL = 7; -} +} \ No newline at end of file