feat: user v2 service query (#7095)

* feat: add query endpoints for user v2 api

* fix: correct integration tests

* fix: correct linting

* fix: correct linting

* fix: comment out permission check on user get and list

* fix: permission check on user v2 query

* fix: merge back origin/main

* fix: add search query in user emails

* fix: reset count for SearchUser if users are removed due to permissions

* fix: reset count for SearchUser if users are removed due to permissions

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Stefan Benz
2024-01-17 10:00:10 +01:00
committed by GitHub
parent 853181155d
commit d9d376a275
18 changed files with 1667 additions and 45 deletions

View File

@@ -47,3 +47,26 @@ func ResourceOwnerFromReq(ctx context.Context, req *object.RequestContext) strin
}
return authz.GetCtxData(ctx).OrgID
}
func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison {
switch method {
case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS:
return query.TextEquals
case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE:
return query.TextEqualsIgnoreCase
case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH:
return query.TextStartsWith
case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH_IGNORE_CASE:
return query.TextStartsWithIgnoreCase
case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS:
return query.TextContains
case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE:
return query.TextContainsIgnoreCase
case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH:
return query.TextEndsWith
case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH_IGNORE_CASE:
return query.TextEndsWithIgnoreCase
default:
return -1
}
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
)
@@ -15,9 +14,8 @@ var _ session.SessionServiceServer = (*Server)(nil)
type Server struct {
session.UnimplementedSessionServiceServer
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
command *command.Commands
query *query.Queries
}
type Config struct{}
@@ -25,12 +23,10 @@ type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
checkPermission: checkPermission,
command: command,
query: query,
}
}

View File

@@ -15,6 +15,7 @@ func UsersToPb(users []*query.User, assetPrefix string) []*user_pb.User {
}
return u
}
func UserToPb(user *query.User, assetPrefix string) *user_pb.User {
return &user_pb.User{
Id: user.ID,

View File

@@ -0,0 +1,328 @@
package user
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
"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"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) {
ctxData := authz.GetCtxData(ctx)
if ctxData.UserID != req.GetUserId() {
if err := s.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, req.GetUserId()); err != nil {
return nil, err
}
}
resp, err := s.query.GetUserByID(ctx, true, req.GetUserId())
if err != nil {
return nil, err
}
return &user.GetUserByIDResponse{
Details: object.DomainToDetailsPb(&domain.ObjectDetails{
Sequence: resp.Sequence,
EventDate: resp.ChangeDate,
ResourceOwner: resp.ResourceOwner,
}),
User: userToPb(resp, s.assetAPIPrefix(ctx)),
}, nil
}
func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) {
queries, err := listUsersRequestToModel(req)
if err != nil {
return nil, err
}
res, err := s.query.SearchUsers(ctx, queries)
if err != nil {
return nil, err
}
res.RemoveNoPermission(ctx, s.checkPermission)
return &user.ListUsersResponse{
Result: 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,
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 {
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,
},
}
}
func machineToPb(userQ *query.Machine) *user.MachineUser {
return &user.MachineUser{
Name: userQ.Name,
Description: userQ.Description,
HasSecret: userQ.Secret != nil,
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.User.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_StateQuery:
return stateQueryToQuery(q.StateQuery)
case *user.SearchQuery_TypeQuery:
return typeQueryToQuery(q.TypeQuery)
case *user.SearchQuery_LoginNameQuery:
return loginNameQueryToQuery(q.LoginNameQuery)
case *user.SearchQuery_ResourceOwner:
return resourceOwnerQueryToQuery(q.ResourceOwner)
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 stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) {
return query.NewUserStateSearchQuery(int32(q.State))
}
func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) {
return query.NewUserTypeSearchQuery(int32(q.Type))
}
func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) {
return query.NewUserLoginNameExistsQuery(q.LoginName, object.TextMethodToQuery(q.Method))
}
func resourceOwnerQueryToQuery(q *user.ResourceOwnerQuery) (query.SearchQuery, error) {
return query.NewUserResourceOwnerSearchQuery(q.OrgID, 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)
}

View File

@@ -0,0 +1,627 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_GetUserByID(t *testing.T) {
orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
type args struct {
ctx context.Context
req *user.GetUserByIDRequest
dep func(ctx context.Context, username string, request *user.GetUserByIDRequest) error
}
tests := []struct {
name string
args args
want *user.GetUserByIDResponse
wantErr bool
}{
{
name: "user by ID, no id provided",
args: args{
IamCTX,
&user.GetUserByIDRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: Tester.Organisation.ID,
},
},
UserId: "",
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) error {
return nil
},
},
wantErr: true,
},
{
name: "user by ID, not found",
args: args{
IamCTX,
&user.GetUserByIDRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: Tester.Organisation.ID,
},
},
UserId: "unknown",
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) error {
return nil
},
},
wantErr: true,
},
{
name: "user by ID, ok",
args: args{
IamCTX,
&user.GetUserByIDRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: Tester.Organisation.ID,
},
},
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) error {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
request.UserId = resp.GetUserId()
return nil
},
},
want: &user.GetUserByIDResponse{
User: &user.User{
State: user.UserState_USER_STATE_ACTIVE,
Username: "",
LoginNames: nil,
PreferredLoginName: "",
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
AvatarUrl: "",
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: orgResp.OrganizationId,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
username := fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())
err := tt.args.dep(tt.args.ctx, username, tt.args.req)
require.NoError(t, err)
var got *user.GetUserByIDResponse
for {
got, err = Client.GetUserByID(tt.args.ctx, tt.args.req)
if err == nil || (tt.wantErr && err != nil) {
break
}
select {
case <-CTX.Done():
t.Fatal(CTX.Err(), err)
case <-time.After(time.Second):
t.Log("retrying GetUserByID")
continue
}
}
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
tt.want.User.UserId = tt.args.req.GetUserId()
tt.want.User.Username = username
tt.want.User.PreferredLoginName = username
tt.want.User.LoginNames = []string{username}
if human := tt.want.User.GetHuman(); human != nil {
human.Email.Email = username
}
require.Equal(t, tt.want.User, got.User)
integration.AssertDetails(t, tt.want, got)
}
})
}
}
type userAttr struct {
UserID string
Username string
}
func TestServer_ListUsers(t *testing.T) {
orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
userResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listusers.com", time.Now().UnixNano()))
type args struct {
ctx context.Context
count int
req *user.ListUsersRequest
dep func(ctx context.Context, org string, usernames []string, request *user.ListUsersRequest) ([]userAttr, error)
}
tests := []struct {
name string
args args
want *user.ListUsersResponse
wantErr bool
}{
{
name: "list user by id, no permission",
args: args{
UserCTX,
0,
&user.ListUsersRequest{},
func(ctx context.Context, org string, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
request.Queries = append(request.Queries, InUserIDsQuery([]string{userResp.UserId}))
return []userAttr{}, nil
},
},
want: &user.ListUsersResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
SortingColumn: 0,
Result: []*user.User{},
},
},
{
name: "list user by id, ok",
args: args{
IamCTX,
1,
&user.ListUsersRequest{},
func(ctx context.Context, org string, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
userIDs := make([]string, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
},
},
want: &user.ListUsersResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
SortingColumn: 0,
Result: []*user.User{
{
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
},
},
},
},
{
name: "list user by id multiple, ok",
args: args{
IamCTX,
3,
&user.ListUsersRequest{},
func(ctx context.Context, org string, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
userIDs := make([]string, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
},
},
want: &user.ListUsersResponse{
Details: &object.ListDetails{
TotalResult: 3,
Timestamp: timestamppb.Now(),
},
SortingColumn: 0,
Result: []*user.User{
{
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
}, {
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
}, {
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
},
},
},
},
{
name: "list user by username, ok",
args: args{
IamCTX,
1,
&user.ListUsersRequest{},
func(ctx context.Context, org string, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
userIDs := make([]string, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username}
request.Queries = append(request.Queries, UsernameQuery(username))
}
return infos, nil
},
},
want: &user.ListUsersResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
SortingColumn: 0,
Result: []*user.User{
{
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
},
},
},
},
{
name: "list user in emails, ok",
args: args{
IamCTX,
1,
&user.ListUsersRequest{},
func(ctx context.Context, org string, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username}
}
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
},
},
want: &user.ListUsersResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
SortingColumn: 0,
Result: []*user.User{
{
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
},
},
},
},
{
name: "list user in emails multiple, ok",
args: args{
IamCTX,
3,
&user.ListUsersRequest{},
func(ctx context.Context, org string, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username}
}
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
},
},
want: &user.ListUsersResponse{
Details: &object.ListDetails{
TotalResult: 3,
Timestamp: timestamppb.Now(),
},
SortingColumn: 0,
Result: []*user.User{
{
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
}, {
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
}, {
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
},
},
},
},
},
{
name: "list user in emails no found, ok",
args: args{
IamCTX,
3,
&user.ListUsersRequest{Queries: []*user.SearchQuery{
InUserEmailsQuery([]string{"notfound"}),
},
},
func(ctx context.Context, org string, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
return []userAttr{}, nil
},
},
want: &user.ListUsersResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
SortingColumn: 0,
Result: []*user.User{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
usernames := make([]string, tt.args.count)
for i := 0; i < tt.args.count; i++ {
usernames[i] = fmt.Sprintf("%d%d@mouse.com", time.Now().UnixNano(), i)
}
infos, err := tt.args.dep(tt.args.ctx, orgResp.OrganizationId, usernames, tt.args.req)
require.NoError(t, err)
var got *user.ListUsersResponse
for {
got, err = Client.ListUsers(tt.args.ctx, tt.args.req)
if err == nil || (tt.wantErr && err != nil) {
break
}
select {
case <-CTX.Done():
t.Fatal(tt.args.ctx.Err(), err)
case <-time.After(time.Second):
t.Log("retrying ListUsers")
continue
}
}
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
// always only give back dependency infos which are required for the response
require.Len(t, tt.want.Result, len(infos))
// always first check length, otherwise its failed anyway
require.Len(t, got.Result, len(tt.want.Result))
// fill in userid and username as it is generated
for i := range infos {
tt.want.Result[i].UserId = infos[i].UserID
tt.want.Result[i].Username = infos[i].Username
tt.want.Result[i].PreferredLoginName = infos[i].Username
tt.want.Result[i].LoginNames = []string{infos[i].Username}
if human := tt.want.Result[i].GetHuman(); human != nil {
human.Email.Email = infos[i].Username
}
}
for i := range tt.want.Result {
require.Contains(t, got.Result, tt.want.Result[i])
}
integration.AssertListDetails(t, tt.want, got)
}
})
}
}
func InUserIDsQuery(ids []string) *user.SearchQuery {
return &user.SearchQuery{Query: &user.SearchQuery_InUserIdsQuery{
InUserIdsQuery: &user.InUserIDQuery{
UserIds: ids,
},
},
}
}
func InUserEmailsQuery(emails []string) *user.SearchQuery {
return &user.SearchQuery{Query: &user.SearchQuery_InUserEmailsQuery{
InUserEmailsQuery: &user.InUserEmailsQuery{
UserEmails: emails,
},
},
}
}
func UsernameQuery(username string) *user.SearchQuery {
return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: username,
},
},
}
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
@@ -23,6 +24,10 @@ type Server struct {
idpAlg crypto.EncryptionAlgorithm
idpCallback func(ctx context.Context) string
samlRootURL func(ctx context.Context, idpID string) string
assetAPIPrefix func(context.Context) string
checkPermission domain.PermissionCheck
}
type Config struct{}
@@ -34,14 +39,18 @@ func CreateServer(
idpAlg crypto.EncryptionAlgorithm,
idpCallback func(ctx context.Context) string,
samlRootURL func(ctx context.Context, idpID string) string,
assetAPIPrefix func(ctx context.Context) string,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
userCodeAlg: userCodeAlg,
idpAlg: idpAlg,
idpCallback: idpCallback,
samlRootURL: samlRootURL,
command: command,
query: query,
userCodeAlg: userCodeAlg,
idpAlg: idpAlg,
idpCallback: idpCallback,
samlRootURL: samlRootURL,
assetAPIPrefix: assetAPIPrefix,
checkPermission: checkPermission,
}
}

View File

@@ -27,10 +27,12 @@ import (
)
var (
CTX context.Context
ErrCTX context.Context
Tester *integration.Tester
Client user.UserServiceClient
CTX context.Context
IamCTX context.Context
UserCTX context.Context
ErrCTX context.Context
Tester *integration.Tester
Client user.UserServiceClient
)
func TestMain(m *testing.M) {
@@ -41,6 +43,8 @@ func TestMain(m *testing.M) {
Tester = integration.NewTester(ctx)
defer Tester.Done()
UserCTX = Tester.WithAuthorization(ctx, integration.Login)
IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
CTX, ErrCTX = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
Client = Tester.Client.UserV2
return m.Run()