mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 08:47:32 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
328
internal/api/grpc/user/v2/query.go
Normal file
328
internal/api/grpc/user/v2/query.go
Normal 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)
|
||||
}
|
627
internal/api/grpc/user/v2/query_integration_test.go
Normal file
627
internal/api/grpc/user/v2/query_integration_test.go
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -13,6 +13,10 @@ type DetailsMsg interface {
|
||||
GetDetails() *object.Details
|
||||
}
|
||||
|
||||
type ListDetailsMsg interface {
|
||||
GetDetails() *object.ListDetails
|
||||
}
|
||||
|
||||
// AssertDetails asserts values in a message's object Details,
|
||||
// if the object Details in expected is a non-nil value.
|
||||
// It targets API v2 messages that have the `GetDetails()` method.
|
||||
@@ -39,3 +43,17 @@ func AssertDetails[D DetailsMsg](t testing.TB, expected, actual D) {
|
||||
|
||||
assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner())
|
||||
}
|
||||
|
||||
func AssertListDetails[D ListDetailsMsg](t testing.TB, expected, actual D) {
|
||||
wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails()
|
||||
if wantDetails == nil {
|
||||
assert.Nil(t, gotDetails)
|
||||
return
|
||||
}
|
||||
|
||||
gotCD := gotDetails.GetTimestamp().AsTime()
|
||||
now := time.Now()
|
||||
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
|
||||
|
||||
assert.Equal(t, wantDetails.GetTotalResult(), gotDetails.GetTotalResult())
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ import (
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
organisation "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
@@ -136,6 +137,63 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string) *org.AddOrganizationResponse {
|
||||
resp, err := s.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{
|
||||
Name: name,
|
||||
Admins: []*org.AddOrganizationRequest_Admin{
|
||||
{
|
||||
UserType: &org.AddOrganizationRequest_Admin_Human{
|
||||
Human: &user.AddHumanUserRequest{
|
||||
Profile: &user.SetHumanProfile{
|
||||
GivenName: "firstname",
|
||||
FamilyName: "lastname",
|
||||
},
|
||||
Email: &user.SetHumanEmail{
|
||||
Email: adminEmail,
|
||||
Verification: &user.SetHumanEmail_ReturnCode{
|
||||
ReturnCode: &user.ReturnEmailVerificationCode{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
logging.OnError(err).Fatal("create org")
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user.AddHumanUserResponse {
|
||||
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
||||
Organization: &object.Organization{
|
||||
Org: &object.Organization_OrgId{
|
||||
OrgId: org,
|
||||
},
|
||||
},
|
||||
Profile: &user.SetHumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: gu.Ptr(user.Gender_GENDER_MALE),
|
||||
},
|
||||
Email: &user.SetHumanEmail{
|
||||
Email: email,
|
||||
Verification: &user.SetHumanEmail_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
Phone: &user.SetHumanPhone{
|
||||
Phone: "+41791234567",
|
||||
Verification: &user.SetHumanPhone_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
logging.OnError(err).Fatal("create human user")
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Tester) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResponse {
|
||||
resp, err := s.Client.Mgmt.AddMachineUser(ctx, &mgmt.AddMachineUserRequest{
|
||||
UserName: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
|
||||
|
@@ -122,6 +122,29 @@ type NotifyUser struct {
|
||||
PasswordSet bool
|
||||
}
|
||||
|
||||
func (u *Users) RemoveNoPermission(ctx context.Context, permissionCheck domain.PermissionCheck) {
|
||||
removableIndexes := make([]int, 0)
|
||||
for i := range u.Users {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
if ctxData.UserID != u.Users[i].ID {
|
||||
if err := permissionCheck(ctx, domain.PermissionUserRead, ctxData.OrgID, u.Users[i].ID); err != nil {
|
||||
removableIndexes = append(removableIndexes, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
removed := 0
|
||||
for _, removeIndex := range removableIndexes {
|
||||
u.Users = removeUser(u.Users, removeIndex-removed)
|
||||
removed++
|
||||
}
|
||||
// reset count as some users could be removed
|
||||
u.SearchResponse.Count = uint64(len(u.Users))
|
||||
}
|
||||
|
||||
func removeUser(slice []*User, s int) []*User {
|
||||
return append(slice[:s], slice[s+1:]...)
|
||||
}
|
||||
|
||||
type UserSearchQueries struct {
|
||||
SearchRequest
|
||||
Queries []SearchQuery
|
||||
@@ -579,7 +602,6 @@ func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries) (
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-AG4gs", "Errors.Internal")
|
||||
}
|
||||
|
||||
users.State, err = q.latestState(ctx, userTable)
|
||||
return users, err
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
@@ -16,6 +18,129 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func Test_RemoveNoPermission(t *testing.T) {
|
||||
type want struct {
|
||||
users []*User
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
want want
|
||||
users *Users
|
||||
permissions []string
|
||||
}{
|
||||
{
|
||||
"permissions for all users",
|
||||
want{
|
||||
users: []*User{
|
||||
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
&Users{
|
||||
Users: []*User{
|
||||
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
[]string{"first", "second", "third"},
|
||||
},
|
||||
{
|
||||
"permissions for one user, first",
|
||||
want{
|
||||
users: []*User{
|
||||
{ID: "first"},
|
||||
},
|
||||
},
|
||||
&Users{
|
||||
Users: []*User{
|
||||
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
[]string{"first"},
|
||||
},
|
||||
{
|
||||
"permissions for one user, second",
|
||||
want{
|
||||
users: []*User{
|
||||
{ID: "second"},
|
||||
},
|
||||
},
|
||||
&Users{
|
||||
Users: []*User{
|
||||
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
[]string{"second"},
|
||||
},
|
||||
{
|
||||
"permissions for one user, third",
|
||||
want{
|
||||
users: []*User{
|
||||
{ID: "third"},
|
||||
},
|
||||
},
|
||||
&Users{
|
||||
Users: []*User{
|
||||
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
[]string{"third"},
|
||||
},
|
||||
{
|
||||
"permissions for two users, first",
|
||||
want{
|
||||
users: []*User{
|
||||
{ID: "first"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
&Users{
|
||||
Users: []*User{
|
||||
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
[]string{"first", "third"},
|
||||
},
|
||||
{
|
||||
"permissions for two users, second",
|
||||
want{
|
||||
users: []*User{
|
||||
{ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
&Users{
|
||||
Users: []*User{
|
||||
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
[]string{"second", "third"},
|
||||
},
|
||||
{
|
||||
"no permissions",
|
||||
want{
|
||||
users: []*User{},
|
||||
},
|
||||
&Users{
|
||||
Users: []*User{
|
||||
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||
},
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
for _, perm := range tt.permissions {
|
||||
if resourceID == perm {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("failed")
|
||||
}
|
||||
tt.users.RemoveNoPermission(context.Background(), checkPermission)
|
||||
require.Equal(t, tt.want.users, tt.users.Users)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
loginNamesQuery = `SELECT login_names.user_id, ARRAY_AGG(login_names.login_name)::TEXT[] AS loginnames, ARRAY_AGG(LOWER(login_names.login_name))::TEXT[] AS loginnames_lower, login_names.instance_id` +
|
||||
` FROM projections.login_names3 AS login_names` +
|
||||
|
Reference in New Issue
Block a user