diff --git a/internal/api/grpc/object/v2/converter.go b/internal/api/grpc/object/v2/converter.go index 753b9db0954..5ae28e4d26c 100644 --- a/internal/api/grpc/object/v2/converter.go +++ b/internal/api/grpc/object/v2/converter.go @@ -39,7 +39,7 @@ func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) if query == nil { return 0, 0, false } - return query.Offset, uint64(query.Limit), query.Asc + return query.Offset, uint64(query.GetLimit()), query.GetAsc() } func ResourceOwnerFromReq(ctx context.Context, req *object.RequestContext) string { diff --git a/internal/api/grpc/user/v2/convert/human.go b/internal/api/grpc/user/v2/convert/human.go new file mode 100644 index 00000000000..e1ab4d170b2 --- /dev/null +++ b/internal/api/grpc/user/v2/convert/human.go @@ -0,0 +1,61 @@ +package convert + +import ( + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func humanToPb(userQ *query.Human, assetPrefix, owner string) *user.HumanUser { + if userQ == nil { + return nil + } + + var passwordChanged, mfaInitSkipped *timestamppb.Timestamp + if !userQ.PasswordChanged.IsZero() { + passwordChanged = timestamppb.New(userQ.PasswordChanged) + } + if !userQ.MFAInitSkipped.IsZero() { + mfaInitSkipped = timestamppb.New(userQ.MFAInitSkipped) + } + return &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: userQ.FirstName, + FamilyName: userQ.LastName, + NickName: gu.Ptr(userQ.NickName), + DisplayName: gu.Ptr(userQ.DisplayName), + PreferredLanguage: gu.Ptr(userQ.PreferredLanguage.String()), + Gender: gu.Ptr(genderToPb(userQ.Gender)), + AvatarUrl: domain.AvatarURL(assetPrefix, owner, userQ.AvatarKey), + }, + Email: &user.HumanEmail{ + Email: string(userQ.Email), + IsVerified: userQ.IsEmailVerified, + }, + Phone: &user.HumanPhone{ + Phone: string(userQ.Phone), + IsVerified: userQ.IsPhoneVerified, + }, + PasswordChangeRequired: userQ.PasswordChangeRequired, + PasswordChanged: passwordChanged, + MfaInitSkipped: mfaInitSkipped, + } +} + +func genderToPb(gender domain.Gender) user.Gender { + switch gender { + case domain.GenderDiverse: + return user.Gender_GENDER_DIVERSE + case domain.GenderFemale: + return user.Gender_GENDER_FEMALE + case domain.GenderMale: + return user.Gender_GENDER_MALE + case domain.GenderUnspecified: + return user.Gender_GENDER_UNSPECIFIED + default: + return user.Gender_GENDER_UNSPECIFIED + } +} diff --git a/internal/api/grpc/user/v2/convert/human_test.go b/internal/api/grpc/user/v2/convert/human_test.go new file mode 100644 index 00000000000..619ee56e145 --- /dev/null +++ b/internal/api/grpc/user/v2/convert/human_test.go @@ -0,0 +1,179 @@ +package convert + +import ( + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_genderToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + input domain.Gender + expect user.Gender + }{ + { + name: "Diverse", + input: domain.GenderDiverse, + expect: user.Gender_GENDER_DIVERSE, + }, + { + name: "Female", + input: domain.GenderFemale, + expect: user.Gender_GENDER_FEMALE, + }, + { + name: "Male", + input: domain.GenderMale, + expect: user.Gender_GENDER_MALE, + }, + { + name: "Unspecified", + input: domain.GenderUnspecified, + expect: user.Gender_GENDER_UNSPECIFIED, + }, + { + name: "Unknown value", + input: domain.Gender(999), + expect: user.Gender_GENDER_UNSPECIFIED, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := genderToPb(tc.input) + require.Equal(t, tc.expect, got) + }) + } +} + +func Test_humanToPb(t *testing.T) { + t.Parallel() + + now := time.Now().UTC() + assetPrefix := "prefix" + owner := "owner" + avatarKey := "avatar-key" + + tt := []struct { + name string + input *query.Human + expected *user.HumanUser + }{ + { + name: "all fields set", + input: &query.Human{ + FirstName: "John", + LastName: "Doe", + NickName: "JD", + DisplayName: "Johnny", + PreferredLanguage: language.English, + Gender: domain.GenderMale, + AvatarKey: avatarKey, + Email: "john.doe@example.com", + IsEmailVerified: true, + Phone: "+123456789", + IsPhoneVerified: true, + PasswordChangeRequired: true, + PasswordChanged: now, + MFAInitSkipped: now, + }, + expected: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "John", + FamilyName: "Doe", + NickName: gu.Ptr("JD"), + DisplayName: gu.Ptr("Johnny"), + PreferredLanguage: gu.Ptr("en"), + Gender: gu.Ptr(user.Gender_GENDER_MALE), + AvatarUrl: domain.AvatarURL(assetPrefix, owner, avatarKey), + }, + Email: &user.HumanEmail{ + Email: "john.doe@example.com", + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+123456789", + IsVerified: true, + }, + PasswordChangeRequired: true, + PasswordChanged: timestamppb.New(now), + MfaInitSkipped: timestamppb.New(now), + }, + }, + { + name: "zero times, not verified, unspecified gender", + input: &query.Human{ + FirstName: "Jane", + LastName: "Smith", + NickName: "", + DisplayName: "", + PreferredLanguage: language.German, + Gender: domain.GenderUnspecified, + AvatarKey: "", + Email: "jane.smith@example.com", + IsEmailVerified: false, + Phone: "", + IsPhoneVerified: false, + PasswordChangeRequired: false, + PasswordChanged: time.Time{}, + MFAInitSkipped: time.Time{}, + }, + expected: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Jane", + FamilyName: "Smith", + NickName: gu.Ptr(""), + DisplayName: gu.Ptr(""), + PreferredLanguage: gu.Ptr("de"), + Gender: gu.Ptr(user.Gender_GENDER_UNSPECIFIED), + AvatarUrl: domain.AvatarURL(assetPrefix, owner, ""), + }, + Email: &user.HumanEmail{ + Email: "jane.smith@example.com", + IsVerified: false, + }, + Phone: &user.HumanPhone{ + Phone: "", + IsVerified: false, + }, + PasswordChangeRequired: false, + PasswordChanged: nil, + MfaInitSkipped: nil, + }, + }, + { + name: "nil input", + input: nil, + expected: nil, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.input == nil { + require.Nil(t, humanToPb(nil, assetPrefix, owner)) + return + } + got := humanToPb(tc.input, assetPrefix, owner) + require.Equal(t, tc.expected.Profile, got.Profile) + require.Equal(t, tc.expected.Email, got.Email) + require.Equal(t, tc.expected.Phone, got.Phone) + require.Equal(t, tc.expected.PasswordChangeRequired, got.PasswordChangeRequired) + require.Equal(t, tc.expected.PasswordChanged, got.PasswordChanged) + require.Equal(t, tc.expected.MfaInitSkipped, got.MfaInitSkipped) + }) + } +} diff --git a/internal/api/grpc/user/v2/convert/machine.go b/internal/api/grpc/user/v2/convert/machine.go new file mode 100644 index 00000000000..23898b1168a --- /dev/null +++ b/internal/api/grpc/user/v2/convert/machine.go @@ -0,0 +1,27 @@ +package convert + +import ( + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func machineToPb(userQ *query.Machine) *user.MachineUser { + return &user.MachineUser{ + Name: userQ.Name, + Description: userQ.Description, + HasSecret: userQ.EncodedSecret != "", + AccessTokenType: accessTokenTypeToPb(userQ.AccessTokenType), + } +} + +func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenType { + switch accessTokenType { + case domain.OIDCTokenTypeBearer: + return user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER + case domain.OIDCTokenTypeJWT: + return user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT + default: + return user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER + } +} diff --git a/internal/api/grpc/user/v2/convert/machine_test.go b/internal/api/grpc/user/v2/convert/machine_test.go new file mode 100644 index 00000000000..b036e03e3fa --- /dev/null +++ b/internal/api/grpc/user/v2/convert/machine_test.go @@ -0,0 +1,108 @@ +package convert + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_accessTokenTypeToPb(t *testing.T) { + t.Parallel() + tt := []struct { + name string + input domain.OIDCTokenType + expected user.AccessTokenType + }{ + { + name: "Bearer token type", + input: domain.OIDCTokenTypeBearer, + expected: user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER, + }, + { + name: "JWT token type", + input: domain.OIDCTokenTypeJWT, + expected: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }, + { + name: "Unknown token type returns Bearer", + input: domain.OIDCTokenType(2), + expected: user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := accessTokenTypeToPb(tc.input) + require.Equal(t, tc.expected, got) + }) + } +} + +func Test_machineToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + input *query.Machine + expected *user.MachineUser + }{ + { + name: "All fields set, Bearer token, HasSecret true", + input: &query.Machine{ + Name: "machine1", + Description: "desc", + EncodedSecret: "secret", + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + expected: &user.MachineUser{ + Name: "machine1", + Description: "desc", + HasSecret: true, + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER, + }, + }, + { + name: "No secret, JWT token", + input: &query.Machine{ + Name: "machine2", + Description: "desc2", + EncodedSecret: "", + AccessTokenType: domain.OIDCTokenTypeJWT, + }, + expected: &user.MachineUser{ + Name: "machine2", + Description: "desc2", + HasSecret: false, + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }, + }, + { + name: "Unknown token type, HasSecret false", + input: &query.Machine{ + Name: "machine3", + Description: "desc3", + EncodedSecret: "", + AccessTokenType: domain.OIDCTokenType(99), + }, + expected: &user.MachineUser{ + Name: "machine3", + Description: "desc3", + HasSecret: false, + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := machineToPb(tc.input) + require.Equal(t, tc.expected, got) + }) + } +} diff --git a/internal/api/grpc/user/v2/convert/user.go b/internal/api/grpc/user/v2/convert/user.go new file mode 100644 index 00000000000..90169e5d67f --- /dev/null +++ b/internal/api/grpc/user/v2/convert/user.go @@ -0,0 +1,191 @@ +package convert + +import ( + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func ListUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) { + offset, limit, asc := object.ListQueryToQuery(req.GetQuery()) + queries, err := userQueriesToQuery(req.GetQueries(), 0 /*start from level 0*/) + if err != nil { + return nil, err + } + return &query.UserSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: userFieldNameToSortingColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func UsersToPb(users []*query.User, assetPrefix string) []*user.User { + u := make([]*user.User, len(users)) + for i, user := range users { + u[i] = UserToPb(user, assetPrefix) + } + return u +} + +func UserToPb(userQ *query.User, assetPrefix string) *user.User { + if userQ == nil { + return nil + } + + return &user.User{ + UserId: userQ.ID, + Details: object.DomainToDetailsPb(&domain.ObjectDetails{ + Sequence: userQ.Sequence, + EventDate: userQ.ChangeDate, + ResourceOwner: userQ.ResourceOwner, + CreationDate: userQ.CreationDate, + }), + State: userStateToPb(userQ.State), + Username: userQ.Username, + LoginNames: userQ.LoginNames, + PreferredLoginName: userQ.PreferredLoginName, + Type: userTypeToPb(userQ, assetPrefix), + } +} + +func userTypeToPb(userQ *query.User, assetPrefix string) user.UserType { + if userQ == nil { + return nil + } + + if userQ.Human != nil { + return &user.User_Human{ + Human: humanToPb(userQ.Human, assetPrefix, userQ.ResourceOwner), + } + } + if userQ.Machine != nil { + return &user.User_Machine{ + Machine: machineToPb(userQ.Machine), + } + } + return nil +} + +func userStateToPb(state domain.UserState) user.UserState { + switch state { + case domain.UserStateActive: + return user.UserState_USER_STATE_ACTIVE + case domain.UserStateInactive: + return user.UserState_USER_STATE_INACTIVE + case domain.UserStateDeleted: + return user.UserState_USER_STATE_DELETED + case domain.UserStateInitial: + return user.UserState_USER_STATE_INITIAL + case domain.UserStateLocked: + return user.UserState_USER_STATE_LOCKED + case domain.UserStateUnspecified: + return user.UserState_USER_STATE_UNSPECIFIED + case domain.UserStateSuspend: + return user.UserState_USER_STATE_UNSPECIFIED + default: + return user.UserState_USER_STATE_UNSPECIFIED + } +} + +func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { + switch field { + case user.UserFieldName_USER_FIELD_NAME_EMAIL: + return query.HumanEmailCol + case user.UserFieldName_USER_FIELD_NAME_FIRST_NAME: + return query.HumanFirstNameCol + case user.UserFieldName_USER_FIELD_NAME_LAST_NAME: + return query.HumanLastNameCol + case user.UserFieldName_USER_FIELD_NAME_DISPLAY_NAME: + return query.HumanDisplayNameCol + case user.UserFieldName_USER_FIELD_NAME_USER_NAME: + return query.UserUsernameCol + case user.UserFieldName_USER_FIELD_NAME_STATE: + return query.UserStateCol + case user.UserFieldName_USER_FIELD_NAME_TYPE: + return query.UserTypeCol + case user.UserFieldName_USER_FIELD_NAME_NICK_NAME: + return query.HumanNickNameCol + case user.UserFieldName_USER_FIELD_NAME_CREATION_DATE: + return query.UserCreationDateCol + case user.UserFieldName_USER_FIELD_NAME_UNSPECIFIED: + return query.UserIDCol + default: + return query.UserIDCol + } +} + +func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = userQueryToQuery(query, level) + if err != nil { + return nil, err + } + } + return q, nil +} + +func userQueryToQuery(sq *user.SearchQuery, level uint8) (query.SearchQuery, error) { + if level > 20 { + // can't go deeper than 20 levels of nesting. + return nil, zerrors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.Query.TooManyNestingLevels") + } + switch q := sq.Query.(type) { + case *user.SearchQuery_UserNameQuery: + return query.NewUserUsernameSearchQuery(q.UserNameQuery.GetUserName(), object.TextMethodToQuery(q.UserNameQuery.GetMethod())) + case *user.SearchQuery_FirstNameQuery: + return query.NewUserFirstNameSearchQuery(q.FirstNameQuery.GetFirstName(), object.TextMethodToQuery(q.FirstNameQuery.GetMethod())) + case *user.SearchQuery_LastNameQuery: + return query.NewUserLastNameSearchQuery(q.LastNameQuery.GetLastName(), object.TextMethodToQuery(q.LastNameQuery.GetMethod())) + case *user.SearchQuery_NickNameQuery: + return query.NewUserNickNameSearchQuery(q.NickNameQuery.GetNickName(), object.TextMethodToQuery(q.NickNameQuery.GetMethod())) + case *user.SearchQuery_DisplayNameQuery: + return query.NewUserDisplayNameSearchQuery(q.DisplayNameQuery.GetDisplayName(), object.TextMethodToQuery(q.DisplayNameQuery.GetMethod())) + case *user.SearchQuery_EmailQuery: + return query.NewUserEmailSearchQuery(q.EmailQuery.GetEmailAddress(), object.TextMethodToQuery(q.EmailQuery.GetMethod())) + case *user.SearchQuery_PhoneQuery: + return query.NewUserPhoneSearchQuery(q.PhoneQuery.GetNumber(), object.TextMethodToQuery(q.PhoneQuery.GetMethod())) + case *user.SearchQuery_StateQuery: + return query.NewUserStateSearchQuery(q.StateQuery.GetState().ToDomain()) + case *user.SearchQuery_TypeQuery: + return query.NewUserTypeSearchQuery(q.TypeQuery.GetType().ToDomain()) + case *user.SearchQuery_LoginNameQuery: + return query.NewUserLoginNameExistsQuery(q.LoginNameQuery.GetLoginName(), object.TextMethodToQuery(q.LoginNameQuery.GetMethod())) + case *user.SearchQuery_OrganizationIdQuery: + return query.NewUserResourceOwnerSearchQuery(q.OrganizationIdQuery.GetOrganizationId(), query.TextEquals) + case *user.SearchQuery_InUserIdsQuery: + return query.NewUserInUserIdsSearchQuery(q.InUserIdsQuery.GetUserIds()) + case *user.SearchQuery_OrQuery: + mappedQueries, err := userQueriesToQuery(q.OrQuery.GetQueries(), level+1) + if err != nil { + return nil, err + } + return query.NewUserOrSearchQuery(mappedQueries) + case *user.SearchQuery_AndQuery: + mappedQueries, err := userQueriesToQuery(q.AndQuery.GetQueries(), level+1) + if err != nil { + return nil, err + } + return query.NewUserAndSearchQuery(mappedQueries) + case *user.SearchQuery_NotQuery: + mappedQuery, err := userQueryToQuery(q.NotQuery.GetQuery(), level+1) + if err != nil { + return nil, err + } + return query.NewUserNotSearchQuery(mappedQuery) + case *user.SearchQuery_InUserEmailsQuery: + return query.NewUserInUserEmailsSearchQuery(q.InUserEmailsQuery.GetUserEmails()) + case *user.SearchQuery_MetadataKeyFilter: + return query.NewUserMetadataKeySearchQuery(q.MetadataKeyFilter.GetKey(), query.TextComparison(q.MetadataKeyFilter.GetMethod())) + case *user.SearchQuery_MetadataValueFilter: + return query.NewUserMetadataValueSearchQuery(q.MetadataValueFilter.GetValue(), query.BytesComparison(q.MetadataValueFilter.GetMethod())) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/user/v2/convert/user_test.go b/internal/api/grpc/user/v2/convert/user_test.go new file mode 100644 index 00000000000..f6d59a9b285 --- /dev/null +++ b/internal/api/grpc/user/v2/convert/user_test.go @@ -0,0 +1,1025 @@ +package convert + +import ( + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_userQueryToQuery(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + input *user.SearchQuery + level uint8 + wantErr bool + }{ + { + name: "UserNameQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "testuser", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "FirstNameQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_FirstNameQuery{ + FirstNameQuery: &user.FirstNameQuery{ + FirstName: "John", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "LastNameQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_LastNameQuery{ + LastNameQuery: &user.LastNameQuery{ + LastName: "Doe", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "NickNameQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_NickNameQuery{ + NickNameQuery: &user.NickNameQuery{ + NickName: "JD", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "DisplayNameQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_DisplayNameQuery{ + DisplayNameQuery: &user.DisplayNameQuery{ + DisplayName: "John Doe", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "EmailQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_EmailQuery{ + EmailQuery: &user.EmailQuery{ + EmailAddress: "john@doe.com", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "PhoneQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_PhoneQuery{ + PhoneQuery: &user.PhoneQuery{ + Number: "123456789", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "StateQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_StateQuery{ + StateQuery: &user.StateQuery{ + State: user.UserState_USER_STATE_ACTIVE, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "TypeQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_TypeQuery{ + TypeQuery: &user.TypeQuery{ + Type: user.Type_TYPE_HUMAN, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "LoginNameQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_LoginNameQuery{ + LoginNameQuery: &user.LoginNameQuery{ + LoginName: "login", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "OrganizationIdQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_OrganizationIdQuery{ + OrganizationIdQuery: &user.OrganizationIdQuery{ + OrganizationId: "org1", + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "InUserIdsQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{ + UserIds: []string{"id1", "id2"}, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "OrQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_OrQuery{ + OrQuery: &user.OrQuery{ + Queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "testuser", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "AndQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_AndQuery{ + AndQuery: &user.AndQuery{ + Queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "testuser", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "NotQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_NotQuery{ + NotQuery: &user.NotQuery{ + Query: &user.SearchQuery{ + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "testuser", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "InUserEmailsQuery", + input: &user.SearchQuery{ + Query: &user.SearchQuery_InUserEmailsQuery{ + InUserEmailsQuery: &user.InUserEmailsQuery{ + UserEmails: []string{"john@doe.com", "jane@doe.com"}, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "MetadataKeyFilter", + input: &user.SearchQuery{ + Query: &user.SearchQuery_MetadataKeyFilter{ + MetadataKeyFilter: &metadata.MetadataKeyFilter{ + Key: "key1", + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "MetadataValueFilter", + input: &user.SearchQuery{ + Query: &user.SearchQuery_MetadataValueFilter{ + MetadataValueFilter: &metadata.MetadataValueFilter{ + Value: []byte("value1"), + Method: filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "TooManyNestingLevels", + input: &user.SearchQuery{ + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "testuser", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + level: 21, + wantErr: true, + }, + { + name: "InvalidQueryType", + input: &user.SearchQuery{ + Query: nil, + }, + level: 0, + wantErr: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got, err := userQueryToQuery(tc.input, tc.level) + if tc.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) + } + }) + } +} + +func Test_userQueriesToQuery(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + queries []*user.SearchQuery + level uint8 + wantErr bool + }{ + { + name: "single valid query", + queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "testuser", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "multiple valid queries", + queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "user1", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + { + Query: &user.SearchQuery_EmailQuery{ + EmailQuery: &user.EmailQuery{ + EmailAddress: "user1@example.com", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + level: 0, + wantErr: false, + }, + { + name: "empty queries slice", + queries: []*user.SearchQuery{}, + level: 0, + wantErr: false, + }, + { + name: "query with error (too many nesting levels)", + queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "testuser", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + level: 21, + wantErr: true, + }, + { + name: "query with invalid type", + queries: []*user.SearchQuery{ + { + Query: nil, + }, + }, + level: 0, + wantErr: true, + }, + { + name: "mixed valid and invalid queries", + queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: "testuser", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + { + Query: nil, + }, + }, + level: 0, + wantErr: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := userQueriesToQuery(tc.queries, tc.level) + if tc.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) + assert.Equal(t, len(tc.queries), len(got)) + } + }) + } +} + +func Test_userFieldNameToSortingColumn(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + input user.UserFieldName + expected query.Column + } + + tt := []testCase{ + { + name: "EMAIL", + input: user.UserFieldName_USER_FIELD_NAME_EMAIL, + expected: query.HumanEmailCol, + }, + { + name: "FIRST_NAME", + input: user.UserFieldName_USER_FIELD_NAME_FIRST_NAME, + expected: query.HumanFirstNameCol, + }, + { + name: "LAST_NAME", + input: user.UserFieldName_USER_FIELD_NAME_LAST_NAME, + expected: query.HumanLastNameCol, + }, + { + name: "DISPLAY_NAME", + input: user.UserFieldName_USER_FIELD_NAME_DISPLAY_NAME, + expected: query.HumanDisplayNameCol, + }, + { + name: "USER_NAME", + input: user.UserFieldName_USER_FIELD_NAME_USER_NAME, + expected: query.UserUsernameCol, + }, + { + name: "STATE", + input: user.UserFieldName_USER_FIELD_NAME_STATE, + expected: query.UserStateCol, + }, + { + name: "TYPE", + input: user.UserFieldName_USER_FIELD_NAME_TYPE, + expected: query.UserTypeCol, + }, + { + name: "NICK_NAME", + input: user.UserFieldName_USER_FIELD_NAME_NICK_NAME, + expected: query.HumanNickNameCol, + }, + { + name: "CREATION_DATE", + input: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE, + expected: query.UserCreationDateCol, + }, + { + name: "UNSPECIFIED", + input: user.UserFieldName_USER_FIELD_NAME_UNSPECIFIED, + expected: query.UserIDCol, + }, + { + name: "Unknown value (default)", + input: user.UserFieldName(999), + expected: query.UserIDCol, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := userFieldNameToSortingColumn(tc.input) + assert.Equal(t, tc.expected, got) + }) + } +} + +func Test_userStateToPb(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + input domain.UserState + expected user.UserState + } + + tt := []testCase{ + { + name: "Active", + input: domain.UserStateActive, + expected: user.UserState_USER_STATE_ACTIVE, + }, + { + name: "Inactive", + input: domain.UserStateInactive, + expected: user.UserState_USER_STATE_INACTIVE, + }, + { + name: "Deleted", + input: domain.UserStateDeleted, + expected: user.UserState_USER_STATE_DELETED, + }, + { + name: "Initial", + input: domain.UserStateInitial, + expected: user.UserState_USER_STATE_INITIAL, + }, + { + name: "Locked", + input: domain.UserStateLocked, + expected: user.UserState_USER_STATE_LOCKED, + }, + { + name: "Unspecified", + input: domain.UserStateUnspecified, + expected: user.UserState_USER_STATE_UNSPECIFIED, + }, + { + name: "Suspend", + input: domain.UserStateSuspend, + expected: user.UserState_USER_STATE_UNSPECIFIED, + }, + { + name: "Unknown value (default)", + input: domain.UserState(999), + expected: user.UserState_USER_STATE_UNSPECIFIED, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := userStateToPb(tc.input) + assert.Equal(t, tc.expected, got) + }) + } +} + +func Test_userTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + inputQueryUser *query.User + expectedUserType user.UserType + }{ + { + testName: "when query user is nil should return nil", + expectedUserType: nil, + }, + { + testName: "when query user is human should return usertype human", + inputQueryUser: &query.User{Human: &query.Human{}}, + expectedUserType: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + NickName: new(string), + DisplayName: new(string), + PreferredLanguage: gu.Ptr("und"), + Gender: gu.Ptr(user.Gender_GENDER_UNSPECIFIED), + }, + Email: &user.HumanEmail{}, + Phone: &user.HumanPhone{}, + }, + }, + }, + { + testName: "when query user is machine should return usertype machine", + inputQueryUser: &query.User{Machine: &query.Machine{}}, + expectedUserType: &user.User_Machine{ + Machine: &user.MachineUser{}, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + res := userTypeToPb(tc.inputQueryUser, "asset") + + assert.Equal(t, tc.expectedUserType, res) + }) + } +} + +func Test_UserToPb(t *testing.T) { + t.Parallel() + + now := time.Now().UTC() + + type args struct { + userQ *query.User + assetPrefix string + } + tt := []struct { + name string + args args + want *user.User + }{ + { + name: "nil userQ returns nil", + args: args{ + userQ: nil, + assetPrefix: "prefix", + }, + want: nil, + }, + { + name: "basic user with human type", + args: args{ + userQ: &query.User{ + ID: "user-id", + Sequence: 1, + ChangeDate: now, + ResourceOwner: "owner", + CreationDate: now, + State: domain.UserStateActive, + Username: "username", + LoginNames: []string{"login1", "login2"}, + PreferredLoginName: "preferred", + Human: &query.Human{}, + }, + assetPrefix: "prefix", + }, + want: &user.User{ + UserId: "user-id", + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.New(now), + ResourceOwner: "owner", + CreationDate: timestamppb.New(now), + }, + State: user.UserState_USER_STATE_ACTIVE, + Username: "username", + LoginNames: []string{"login1", "login2"}, + PreferredLoginName: "preferred", + Type: &user.User_Human{ + Human: humanToPb(&query.Human{}, "prefix", "owner"), + }, + }, + }, + { + name: "basic user with machine type", + args: args{ + userQ: &query.User{ + ID: "user-id2", + Sequence: 2, + ChangeDate: now, + ResourceOwner: "owner2", + CreationDate: now, + State: domain.UserStateInactive, + Username: "machineuser", + LoginNames: []string{"machine1"}, + PreferredLoginName: "machinepreferred", + Machine: &query.Machine{}, + }, + assetPrefix: "prefix2", + }, + want: &user.User{ + UserId: "user-id2", + Details: &object.Details{ + Sequence: 2, + ChangeDate: timestamppb.New(now), + ResourceOwner: "owner2", + CreationDate: timestamppb.New(now), + }, + State: user.UserState_USER_STATE_INACTIVE, + Username: "machineuser", + LoginNames: []string{"machine1"}, + PreferredLoginName: "machinepreferred", + Type: &user.User_Machine{ + Machine: machineToPb(&query.Machine{}), + }, + }, + }, + { + name: "user with no type returns nil Type", + args: args{ + userQ: &query.User{ + ID: "user-id3", + Sequence: 3, + ChangeDate: now, + ResourceOwner: "owner3", + CreationDate: now, + State: domain.UserStateDeleted, + Username: "notypeuser", + LoginNames: []string{"notype1"}, + PreferredLoginName: "notypepreferred", + }, + assetPrefix: "prefix3", + }, + want: &user.User{ + UserId: "user-id3", + Details: &object.Details{ + Sequence: 3, + ChangeDate: timestamppb.New(now), + ResourceOwner: "owner3", + CreationDate: timestamppb.New(now), + }, + State: user.UserState_USER_STATE_DELETED, + Username: "notypeuser", + LoginNames: []string{"notype1"}, + PreferredLoginName: "notypepreferred", + Type: nil, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := UserToPb(tc.args.userQ, tc.args.assetPrefix) + assert.Equal(t, tc.want, got) + }) + } +} + +func Test_UsersToPb(t *testing.T) { + t.Parallel() + + now := time.Now().UTC() + + type args struct { + users []*query.User + assetPrefix string + } + tt := []struct { + name string + args args + want []*user.User + }{ + { + name: "nil slice returns nils", + args: args{ + users: nil, + assetPrefix: "prefix", + }, + want: []*user.User{}, + }, + { + name: "empty slice returns empty slice", + args: args{ + users: []*query.User{}, + assetPrefix: "prefix", + }, + want: []*user.User{}, + }, + { + name: "slice with nil user returns slice with nil", + args: args{ + users: []*query.User{nil}, + assetPrefix: "prefix", + }, + want: []*user.User{nil}, + }, + { + name: "slice with one human user", + args: args{ + users: []*query.User{ + { + ID: "user-id", + Sequence: 1, + ChangeDate: now, + ResourceOwner: "owner", + CreationDate: now, + State: domain.UserStateActive, + Username: "username", + LoginNames: []string{"login1", "login2"}, + PreferredLoginName: "preferred", + Human: &query.Human{}, + }, + }, + assetPrefix: "prefix", + }, + want: []*user.User{ + { + UserId: "user-id", + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.New(now), + ResourceOwner: "owner", + CreationDate: timestamppb.New(now), + }, + State: user.UserState_USER_STATE_ACTIVE, + Username: "username", + LoginNames: []string{"login1", "login2"}, + PreferredLoginName: "preferred", + Type: &user.User_Human{ + Human: humanToPb(&query.Human{}, "prefix", "owner"), + }, + }, + }, + }, + { + name: "slice with one machine user", + args: args{ + users: []*query.User{ + { + ID: "user-id2", + Sequence: 2, + ChangeDate: now, + ResourceOwner: "owner2", + CreationDate: now, + State: domain.UserStateInactive, + Username: "machineuser", + LoginNames: []string{"machine1"}, + PreferredLoginName: "machinepreferred", + Machine: &query.Machine{}, + }, + }, + assetPrefix: "prefix2", + }, + want: []*user.User{ + { + UserId: "user-id2", + Details: &object.Details{ + Sequence: 2, + ChangeDate: timestamppb.New(now), + ResourceOwner: "owner2", + CreationDate: timestamppb.New(now), + }, + State: user.UserState_USER_STATE_INACTIVE, + Username: "machineuser", + LoginNames: []string{"machine1"}, + PreferredLoginName: "machinepreferred", + Type: &user.User_Machine{ + Machine: machineToPb(&query.Machine{}), + }, + }, + }, + }, + { + name: "slice with mixed users", + args: args{ + users: []*query.User{ + nil, + { + ID: "user-id3", + Sequence: 3, + ChangeDate: now, + ResourceOwner: "owner3", + CreationDate: now, + State: domain.UserStateDeleted, + Username: "notypeuser", + LoginNames: []string{"notype1"}, + PreferredLoginName: "notypepreferred", + }, + }, + assetPrefix: "prefix3", + }, + want: []*user.User{ + nil, + { + UserId: "user-id3", + Details: &object.Details{ + Sequence: 3, + ChangeDate: timestamppb.New(now), + ResourceOwner: "owner3", + CreationDate: timestamppb.New(now), + }, + State: user.UserState_USER_STATE_DELETED, + Username: "notypeuser", + LoginNames: []string{"notype1"}, + PreferredLoginName: "notypepreferred", + Type: nil, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := UsersToPb(tc.args.users, tc.args.assetPrefix) + assert.Equal(t, tc.want, got) + }) + } +} + +func Test_ListUsersRequestToModel(t *testing.T) { + t.Parallel() + + firstNameQuery, err := query.NewUserFirstNameSearchQuery("first name", query.TextEquals) + require.Nil(t, err) + + type args struct { + req *user.ListUsersRequest + } + tt := []struct { + name string + args args + want *query.UserSearchQueries + wantErr bool + }{ + { + name: "valid request with one query", + args: args{ + req: &user.ListUsersRequest{ + Query: &object.ListQuery{ + Offset: 10, + Limit: 5, + Asc: true, + }, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_EMAIL, + Queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_FirstNameQuery{ + FirstNameQuery: &user.FirstNameQuery{ + FirstName: "first name", + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + }, + }, + want: &query.UserSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 10, + Limit: 5, + Asc: true, + SortingColumn: query.HumanEmailCol, + }, + Queries: []query.SearchQuery{firstNameQuery}, + }, + wantErr: false, + }, + { + name: "valid request with no queries", + args: args{ + req: &user.ListUsersRequest{ + Query: &object.ListQuery{Offset: 0, Limit: 0, Asc: false}, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_UNSPECIFIED, + Queries: []*user.SearchQuery{}, + }, + }, + want: &query.UserSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 0, + Asc: false, + SortingColumn: query.UserIDCol, + }, + Queries: []query.SearchQuery{}, + }, + wantErr: false, + }, + { + name: "invalid query type returns error", + args: args{ + req: &user.ListUsersRequest{ + Query: &object.ListQuery{Offset: 0, Limit: 0, Asc: false}, + SortingColumn: user.UserFieldName_USER_FIELD_NAME_EMAIL, + Queries: []*user.SearchQuery{ + {Query: nil}, + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "nil request returns zero values", + args: args{ + req: nil, + }, + want: &query.UserSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 0, + Asc: false, + SortingColumn: query.UserIDCol, + }, + Queries: []query.SearchQuery{}, + }, + wantErr: false, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := ListUsersRequestToModel(tc.args.req) + if tc.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.want, got) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/user_query.go b/internal/api/grpc/user/v2/user_query.go index 5f5603af310..ea0e698f142 100644 --- a/internal/api/grpc/user/v2/user_query.go +++ b/internal/api/grpc/user/v2/user_query.go @@ -4,13 +4,10 @@ import ( "context" "connectrpc.com/connect" - "github.com/muhlemmer/gu" - "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/api/grpc/user/v2/convert" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) @@ -26,12 +23,12 @@ func (s *Server) GetUserByID(ctx context.Context, req *connect.Request[user.GetU EventDate: resp.ChangeDate, ResourceOwner: resp.ResourceOwner, }), - User: userToPb(resp, s.assetAPIPrefix(ctx)), + User: convert.UserToPb(resp, s.assetAPIPrefix(ctx)), }), nil } func (s *Server) ListUsers(ctx context.Context, req *connect.Request[user.ListUsersRequest]) (*connect.Response[user.ListUsersResponse], error) { - queries, err := listUsersRequestToModel(req.Msg) + queries, err := convert.ListUsersRequestToModel(req.Msg) if err != nil { return nil, err } @@ -40,305 +37,7 @@ func (s *Server) ListUsers(ctx context.Context, req *connect.Request[user.ListUs return nil, err } return connect.NewResponse(&user.ListUsersResponse{ - Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)), + Result: convert.UsersToPb(res.Users, s.assetAPIPrefix(ctx)), Details: object.ToListDetails(res.SearchResponse), }), nil } - -func UsersToPb(users []*query.User, assetPrefix string) []*user.User { - u := make([]*user.User, len(users)) - for i, user := range users { - u[i] = userToPb(user, assetPrefix) - } - return u -} - -func userToPb(userQ *query.User, assetPrefix string) *user.User { - return &user.User{ - UserId: userQ.ID, - Details: object.DomainToDetailsPb(&domain.ObjectDetails{ - Sequence: userQ.Sequence, - EventDate: userQ.ChangeDate, - ResourceOwner: userQ.ResourceOwner, - CreationDate: userQ.CreationDate, - }), - State: userStateToPb(userQ.State), - Username: userQ.Username, - LoginNames: userQ.LoginNames, - PreferredLoginName: userQ.PreferredLoginName, - Type: userTypeToPb(userQ, assetPrefix), - } -} - -func userTypeToPb(userQ *query.User, assetPrefix string) user.UserType { - if userQ.Human != nil { - return &user.User_Human{ - Human: humanToPb(userQ.Human, assetPrefix, userQ.ResourceOwner), - } - } - if userQ.Machine != nil { - return &user.User_Machine{ - Machine: machineToPb(userQ.Machine), - } - } - return nil -} - -func humanToPb(userQ *query.Human, assetPrefix, owner string) *user.HumanUser { - var passwordChanged, mfaInitSkipped *timestamppb.Timestamp - if !userQ.PasswordChanged.IsZero() { - passwordChanged = timestamppb.New(userQ.PasswordChanged) - } - if !userQ.MFAInitSkipped.IsZero() { - mfaInitSkipped = timestamppb.New(userQ.MFAInitSkipped) - } - return &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: userQ.FirstName, - FamilyName: userQ.LastName, - NickName: gu.Ptr(userQ.NickName), - DisplayName: gu.Ptr(userQ.DisplayName), - PreferredLanguage: gu.Ptr(userQ.PreferredLanguage.String()), - Gender: gu.Ptr(genderToPb(userQ.Gender)), - AvatarUrl: domain.AvatarURL(assetPrefix, owner, userQ.AvatarKey), - }, - Email: &user.HumanEmail{ - Email: string(userQ.Email), - IsVerified: userQ.IsEmailVerified, - }, - Phone: &user.HumanPhone{ - Phone: string(userQ.Phone), - IsVerified: userQ.IsPhoneVerified, - }, - PasswordChangeRequired: userQ.PasswordChangeRequired, - PasswordChanged: passwordChanged, - MfaInitSkipped: mfaInitSkipped, - } -} - -func machineToPb(userQ *query.Machine) *user.MachineUser { - return &user.MachineUser{ - Name: userQ.Name, - Description: userQ.Description, - HasSecret: userQ.EncodedSecret != "", - AccessTokenType: accessTokenTypeToPb(userQ.AccessTokenType), - } -} - -func userStateToPb(state domain.UserState) user.UserState { - switch state { - case domain.UserStateActive: - return user.UserState_USER_STATE_ACTIVE - case domain.UserStateInactive: - return user.UserState_USER_STATE_INACTIVE - case domain.UserStateDeleted: - return user.UserState_USER_STATE_DELETED - case domain.UserStateInitial: - return user.UserState_USER_STATE_INITIAL - case domain.UserStateLocked: - return user.UserState_USER_STATE_LOCKED - case domain.UserStateUnspecified: - return user.UserState_USER_STATE_UNSPECIFIED - case domain.UserStateSuspend: - return user.UserState_USER_STATE_UNSPECIFIED - default: - return user.UserState_USER_STATE_UNSPECIFIED - } -} - -func genderToPb(gender domain.Gender) user.Gender { - switch gender { - case domain.GenderDiverse: - return user.Gender_GENDER_DIVERSE - case domain.GenderFemale: - return user.Gender_GENDER_FEMALE - case domain.GenderMale: - return user.Gender_GENDER_MALE - case domain.GenderUnspecified: - return user.Gender_GENDER_UNSPECIFIED - default: - return user.Gender_GENDER_UNSPECIFIED - } -} - -func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenType { - switch accessTokenType { - case domain.OIDCTokenTypeBearer: - return user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER - case domain.OIDCTokenTypeJWT: - return user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT - default: - return user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER - } -} - -func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) { - offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) - if err != nil { - return nil, err - } - return &query.UserSearchQueries{ - SearchRequest: query.SearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - SortingColumn: userFieldNameToSortingColumn(req.SortingColumn), - }, - Queries: queries, - }, nil -} - -func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { - switch field { - case user.UserFieldName_USER_FIELD_NAME_EMAIL: - return query.HumanEmailCol - case user.UserFieldName_USER_FIELD_NAME_FIRST_NAME: - return query.HumanFirstNameCol - case user.UserFieldName_USER_FIELD_NAME_LAST_NAME: - return query.HumanLastNameCol - case user.UserFieldName_USER_FIELD_NAME_DISPLAY_NAME: - return query.HumanDisplayNameCol - case user.UserFieldName_USER_FIELD_NAME_USER_NAME: - return query.UserUsernameCol - case user.UserFieldName_USER_FIELD_NAME_STATE: - return query.UserStateCol - case user.UserFieldName_USER_FIELD_NAME_TYPE: - return query.UserTypeCol - case user.UserFieldName_USER_FIELD_NAME_NICK_NAME: - return query.HumanNickNameCol - case user.UserFieldName_USER_FIELD_NAME_CREATION_DATE: - return query.UserCreationDateCol - case user.UserFieldName_USER_FIELD_NAME_UNSPECIFIED: - return query.UserIDCol - default: - return query.UserIDCol - } -} - -func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { - q := make([]query.SearchQuery, len(queries)) - for i, query := range queries { - q[i], err = userQueryToQuery(query, level) - if err != nil { - return nil, err - } - } - return q, nil -} - -func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) { - if level > 20 { - // can't go deeper than 20 levels of nesting. - return nil, zerrors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.Query.TooManyNestingLevels") - } - switch q := query.Query.(type) { - case *user.SearchQuery_UserNameQuery: - return userNameQueryToQuery(q.UserNameQuery) - case *user.SearchQuery_FirstNameQuery: - return firstNameQueryToQuery(q.FirstNameQuery) - case *user.SearchQuery_LastNameQuery: - return lastNameQueryToQuery(q.LastNameQuery) - case *user.SearchQuery_NickNameQuery: - return nickNameQueryToQuery(q.NickNameQuery) - case *user.SearchQuery_DisplayNameQuery: - return displayNameQueryToQuery(q.DisplayNameQuery) - case *user.SearchQuery_EmailQuery: - return emailQueryToQuery(q.EmailQuery) - case *user.SearchQuery_PhoneQuery: - return phoneQueryToQuery(q.PhoneQuery) - case *user.SearchQuery_StateQuery: - return stateQueryToQuery(q.StateQuery) - case *user.SearchQuery_TypeQuery: - return typeQueryToQuery(q.TypeQuery) - case *user.SearchQuery_LoginNameQuery: - return loginNameQueryToQuery(q.LoginNameQuery) - case *user.SearchQuery_OrganizationIdQuery: - return resourceOwnerQueryToQuery(q.OrganizationIdQuery) - case *user.SearchQuery_InUserIdsQuery: - return inUserIdsQueryToQuery(q.InUserIdsQuery) - case *user.SearchQuery_OrQuery: - return orQueryToQuery(q.OrQuery, level) - case *user.SearchQuery_AndQuery: - return andQueryToQuery(q.AndQuery, level) - case *user.SearchQuery_NotQuery: - return notQueryToQuery(q.NotQuery, level) - case *user.SearchQuery_InUserEmailsQuery: - return inUserEmailsQueryToQuery(q.InUserEmailsQuery) - default: - return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") - } -} - -func userNameQueryToQuery(q *user.UserNameQuery) (query.SearchQuery, error) { - return query.NewUserUsernameSearchQuery(q.UserName, object.TextMethodToQuery(q.Method)) -} - -func firstNameQueryToQuery(q *user.FirstNameQuery) (query.SearchQuery, error) { - return query.NewUserFirstNameSearchQuery(q.FirstName, object.TextMethodToQuery(q.Method)) -} - -func lastNameQueryToQuery(q *user.LastNameQuery) (query.SearchQuery, error) { - return query.NewUserLastNameSearchQuery(q.LastName, object.TextMethodToQuery(q.Method)) -} - -func nickNameQueryToQuery(q *user.NickNameQuery) (query.SearchQuery, error) { - return query.NewUserNickNameSearchQuery(q.NickName, object.TextMethodToQuery(q.Method)) -} - -func displayNameQueryToQuery(q *user.DisplayNameQuery) (query.SearchQuery, error) { - return query.NewUserDisplayNameSearchQuery(q.DisplayName, object.TextMethodToQuery(q.Method)) -} - -func emailQueryToQuery(q *user.EmailQuery) (query.SearchQuery, error) { - return query.NewUserEmailSearchQuery(q.EmailAddress, object.TextMethodToQuery(q.Method)) -} - -func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { - return query.NewUserPhoneSearchQuery(q.Number, object.TextMethodToQuery(q.Method)) -} - -func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(q.State.ToDomain()) -} - -func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(q.Type.ToDomain()) -} - -func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) { - return query.NewUserLoginNameExistsQuery(q.LoginName, object.TextMethodToQuery(q.Method)) -} - -func resourceOwnerQueryToQuery(q *user.OrganizationIdQuery) (query.SearchQuery, error) { - return query.NewUserResourceOwnerSearchQuery(q.OrganizationId, query.TextEquals) -} - -func inUserIdsQueryToQuery(q *user.InUserIDQuery) (query.SearchQuery, error) { - return query.NewUserInUserIdsSearchQuery(q.UserIds) -} -func orQueryToQuery(q *user.OrQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userQueriesToQuery(q.Queries, level+1) - if err != nil { - return nil, err - } - return query.NewUserOrSearchQuery(mappedQueries) -} -func andQueryToQuery(q *user.AndQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userQueriesToQuery(q.Queries, level+1) - if err != nil { - return nil, err - } - return query.NewUserAndSearchQuery(mappedQueries) -} -func notQueryToQuery(q *user.NotQuery, level uint8) (query.SearchQuery, error) { - mappedQuery, err := userQueryToQuery(q.Query, level+1) - if err != nil { - return nil, err - } - return query.NewUserNotSearchQuery(mappedQuery) -} - -func inUserEmailsQueryToQuery(q *user.InUserEmailsQuery) (query.SearchQuery, error) { - return query.NewUserInUserEmailsSearchQuery(q.UserEmails) -}