mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-24 03:06:47 +00:00
feat(group): manage users in user groups (#10940)
# Which Problems Are Solved 1. Adding users to user groups and removing users from user groups. 2. Searching for users in user groups by group IDs or user IDs # How the Problems Are Solved By adding: 1. The API definitions to manage users in users groups 3. The command-layer implementation of adding users/removing users to/from user groups. 4. The projection table group_users1 5. Query-side implementation to search for users in user groups # Additional Changes 1. Remove debug statements from unit tests. 2. Fix removal of groups when orgs are removed 3. Add unit tests for groups projection # Additional Context * Related to #9702 * Follow-up for PRs * https://github.com/zitadel/zitadel/pull/10455 * https://github.com/zitadel/zitadel/pull/10758 * https://github.com/zitadel/zitadel/pull/10853
This commit is contained in:
@@ -1383,6 +1383,9 @@ InternalAuthZ:
|
|||||||
- "group.write"
|
- "group.write"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
- "group.delete"
|
- "group.delete"
|
||||||
|
- "group.user.write"
|
||||||
|
- "group.user.read"
|
||||||
|
- "group.user.delete"
|
||||||
- Role: "IAM_OWNER_VIEWER"
|
- Role: "IAM_OWNER_VIEWER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "iam.read"
|
- "iam.read"
|
||||||
@@ -1420,6 +1423,7 @@ InternalAuthZ:
|
|||||||
- "userschema.read"
|
- "userschema.read"
|
||||||
- "session.read"
|
- "session.read"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
- Role: "IAM_ORG_MANAGER"
|
- Role: "IAM_ORG_MANAGER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "org.read"
|
- "org.read"
|
||||||
@@ -1483,6 +1487,9 @@ InternalAuthZ:
|
|||||||
- "group.write"
|
- "group.write"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
- "group.delete"
|
- "group.delete"
|
||||||
|
- "group.user.write"
|
||||||
|
- "group.user.read"
|
||||||
|
- "group.user.delete"
|
||||||
- Role: "IAM_USER_MANAGER"
|
- Role: "IAM_USER_MANAGER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "org.read"
|
- "org.read"
|
||||||
@@ -1512,6 +1519,7 @@ InternalAuthZ:
|
|||||||
- "session.read"
|
- "session.read"
|
||||||
- "session.delete"
|
- "session.delete"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
- Role: "IAM_ADMIN_IMPERSONATOR"
|
- Role: "IAM_ADMIN_IMPERSONATOR"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "admin.impersonation"
|
- "admin.impersonation"
|
||||||
@@ -1580,6 +1588,9 @@ InternalAuthZ:
|
|||||||
- "group.write"
|
- "group.write"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
- "group.delete"
|
- "group.delete"
|
||||||
|
- "group.user.write"
|
||||||
|
- "group.user.read"
|
||||||
|
- "group.user.delete"
|
||||||
- Role: "IAM_LOGIN_CLIENT"
|
- Role: "IAM_LOGIN_CLIENT"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "iam.read"
|
- "iam.read"
|
||||||
@@ -1619,6 +1630,7 @@ InternalAuthZ:
|
|||||||
- "session.delete"
|
- "session.delete"
|
||||||
- "userschema.read"
|
- "userschema.read"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
- Role: "ORG_USER_MANAGER"
|
- Role: "ORG_USER_MANAGER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "org.read"
|
- "org.read"
|
||||||
@@ -1639,6 +1651,7 @@ InternalAuthZ:
|
|||||||
- "session.read"
|
- "session.read"
|
||||||
- "session.delete"
|
- "session.delete"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
- Role: "ORG_OWNER_VIEWER"
|
- Role: "ORG_OWNER_VIEWER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "org.read"
|
- "org.read"
|
||||||
@@ -1661,6 +1674,7 @@ InternalAuthZ:
|
|||||||
- "project.grant.member.read"
|
- "project.grant.member.read"
|
||||||
- "project.grant.user.grant.read"
|
- "project.grant.user.grant.read"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
- Role: "ORG_SETTINGS_MANAGER"
|
- Role: "ORG_SETTINGS_MANAGER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "org.read"
|
- "org.read"
|
||||||
@@ -1692,6 +1706,7 @@ InternalAuthZ:
|
|||||||
- "project.grant.read"
|
- "project.grant.read"
|
||||||
- "project.grant.member.read"
|
- "project.grant.member.read"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
- Role: "ORG_PROJECT_PERMISSION_EDITOR"
|
- Role: "ORG_PROJECT_PERMISSION_EDITOR"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "org.read"
|
- "org.read"
|
||||||
@@ -1961,6 +1976,9 @@ SystemAuthZ:
|
|||||||
- "group.write"
|
- "group.write"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
- "group.delete"
|
- "group.delete"
|
||||||
|
- "group.user.write"
|
||||||
|
- "group.user.read"
|
||||||
|
- "group.user.delete"
|
||||||
- Role: "IAM_OWNER_VIEWER"
|
- Role: "IAM_OWNER_VIEWER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "iam.read"
|
- "iam.read"
|
||||||
@@ -1998,6 +2016,7 @@ SystemAuthZ:
|
|||||||
- "userschema.read"
|
- "userschema.read"
|
||||||
- "session.read"
|
- "session.read"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
- Role: "IAM_ORG_MANAGER"
|
- Role: "IAM_ORG_MANAGER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "org.read"
|
- "org.read"
|
||||||
@@ -2061,6 +2080,9 @@ SystemAuthZ:
|
|||||||
- "group.write"
|
- "group.write"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
- "group.delete"
|
- "group.delete"
|
||||||
|
- "group.user.write"
|
||||||
|
- "group.user.read"
|
||||||
|
- "group.user.delete"
|
||||||
- Role: "IAM_USER_MANAGER"
|
- Role: "IAM_USER_MANAGER"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "org.read"
|
- "org.read"
|
||||||
@@ -2090,6 +2112,7 @@ SystemAuthZ:
|
|||||||
- "session.read"
|
- "session.read"
|
||||||
- "session.delete"
|
- "session.delete"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
- Role: "IAM_ADMIN_IMPERSONATOR"
|
- Role: "IAM_ADMIN_IMPERSONATOR"
|
||||||
Permissions:
|
Permissions:
|
||||||
- "admin.impersonation"
|
- "admin.impersonation"
|
||||||
@@ -2136,6 +2159,7 @@ SystemAuthZ:
|
|||||||
- "session.delete"
|
- "session.delete"
|
||||||
- "userschema.read"
|
- "userschema.read"
|
||||||
- "group.read"
|
- "group.read"
|
||||||
|
- "group.user.read"
|
||||||
|
|
||||||
# If a new projection is introduced it will be prefilled during the setup process (if enabled)
|
# If a new projection is introduced it will be prefilled during the setup process (if enabled)
|
||||||
# This can prevent serving outdated data after a version upgrade, but might require a longer setup / upgrade process:
|
# This can prevent serving outdated data after a version upgrade, but might require a longer setup / upgrade process:
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ func (s *Server) RemoveMyUser(ctx context.Context, _ *auth_pb.RemoveMyUserReques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
details, err := s.command.RemoveUser(ctx, ctxData.UserID, ctxData.ResourceOwner, cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants)...)
|
groupIDs, err := s.getGroupsByUserID(ctx, ctxData.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
details, err := s.command.RemoveUser(ctx, ctxData.UserID, ctxData.ResourceOwner, cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), groupIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -321,3 +325,27 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string {
|
|||||||
}
|
}
|
||||||
return converted
|
return converted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) getGroupsByUserID(ctx context.Context, userID string) ([]string, error) {
|
||||||
|
groupUserQuery, err := query.NewGroupUsersUserIDsSearchQuery([]string{userID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groupUsers, err := s.query.SearchGroupUsers(ctx,
|
||||||
|
&query.GroupUsersSearchQuery{
|
||||||
|
Queries: []query.SearchQuery{
|
||||||
|
groupUserQuery,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(groupUsers.GroupUsers) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
groupIDs := make([]string, 0, len(groupUsers.GroupUsers))
|
||||||
|
for _, groupUser := range groupUsers.GroupUsers {
|
||||||
|
groupIDs = append(groupIDs, groupUser.GroupID)
|
||||||
|
}
|
||||||
|
return groupIDs, nil
|
||||||
|
}
|
||||||
|
|||||||
30
internal/api/grpc/group/v2/group_users.go
Normal file
30
internal/api/grpc/group/v2/group_users.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
group_v2 "github.com/zitadel/zitadel/pkg/grpc/group/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) AddUsersToGroup(ctx context.Context, c *connect.Request[group_v2.AddUsersToGroupRequest]) (*connect.Response[group_v2.AddUsersToGroupResponse], error) {
|
||||||
|
addUsersResp, err := s.command.AddUsersToGroup(ctx, c.Msg.GetId(), c.Msg.GetUserIds())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return connect.NewResponse(&group_v2.AddUsersToGroupResponse{
|
||||||
|
ChangeDate: timestamppb.New(addUsersResp.EventDate),
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) RemoveUsersFromGroup(ctx context.Context, c *connect.Request[group_v2.RemoveUsersFromGroupRequest]) (*connect.Response[group_v2.RemoveUsersFromGroupResponse], error) {
|
||||||
|
details, err := s.command.RemoveUsersFromGroup(ctx, c.Msg.GetId(), c.Msg.GetUserIds())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return connect.NewResponse(&group_v2.RemoveUsersFromGroupResponse{
|
||||||
|
ChangeDate: timestamppb.New(details.EventDate),
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package group_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/golang/protobuf/ptypes/timestamp"
|
"github.com/golang/protobuf/ptypes/timestamp"
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
@@ -32,7 +33,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
wantResp bool
|
wantResp bool
|
||||||
wantGroupID string
|
wantGroupID string
|
||||||
wantErrCode codes.Code
|
wantErrCode codes.Code
|
||||||
wantErrMsg string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "unauthenticated, error",
|
name: "unauthenticated, error",
|
||||||
@@ -42,7 +42,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
OrganizationId: orgResp.GetOrganizationId(),
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.Unauthenticated,
|
wantErrCode: codes.Unauthenticated,
|
||||||
wantErrMsg: "auth header missing",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid name, error",
|
name: "invalid name, error",
|
||||||
@@ -52,7 +51,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
OrganizationId: "org1",
|
OrganizationId: "org1",
|
||||||
},
|
},
|
||||||
wantErrCode: codes.InvalidArgument,
|
wantErrCode: codes.InvalidArgument,
|
||||||
wantErrMsg: "Errors.Group.InvalidName (GROUP-m177lN)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing organization id, error",
|
name: "missing organization id, error",
|
||||||
@@ -61,7 +59,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
Name: integration.GroupName(),
|
Name: integration.GroupName(),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.InvalidArgument,
|
wantErrCode: codes.InvalidArgument,
|
||||||
wantErrMsg: "invalid CreateGroupRequest.OrganizationId: value length must be between 1 and 200 runes, inclusive",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing permission, error",
|
name: "missing permission, error",
|
||||||
@@ -71,7 +68,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
OrganizationId: orgResp.GetOrganizationId(),
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.NotFound,
|
wantErrCode: codes.NotFound,
|
||||||
wantErrMsg: "membership not found (AUTHZ-cdgFk)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "organization not found, error",
|
name: "organization not found, error",
|
||||||
@@ -81,7 +77,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
OrganizationId: "org1",
|
OrganizationId: "org1",
|
||||||
},
|
},
|
||||||
wantErrCode: codes.FailedPrecondition,
|
wantErrCode: codes.FailedPrecondition,
|
||||||
wantErrMsg: "Organisation not found (CMDGRP-j1mH8l)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "instance owner, already existing group (unique name constraint), error",
|
name: "instance owner, already existing group (unique name constraint), error",
|
||||||
@@ -91,7 +86,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
OrganizationId: orgResp.GetOrganizationId(),
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.AlreadyExists,
|
wantErrCode: codes.AlreadyExists,
|
||||||
wantErrMsg: "Errors.Group.AlreadyExists (V3-DKcYh)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "instance owner, already existing group ID, error",
|
name: "instance owner, already existing group ID, error",
|
||||||
@@ -102,7 +96,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
OrganizationId: orgResp.GetOrganizationId(),
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.AlreadyExists,
|
wantErrCode: codes.AlreadyExists,
|
||||||
wantErrMsg: "Errors.Group.AlreadyExists (CMDGRP-shRut3)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "organization owner, missing permission, error",
|
name: "organization owner, missing permission, error",
|
||||||
@@ -112,7 +105,6 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
OrganizationId: orgResp.GetOrganizationId(),
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.NotFound,
|
wantErrCode: codes.NotFound,
|
||||||
wantErrMsg: "membership not found (AUTHZ-cdgFk)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "organization owner, with permission, ok",
|
name: "organization owner, with permission, ok",
|
||||||
@@ -146,13 +138,14 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
beforeCreationDate := time.Now().UTC()
|
||||||
got, err := instance.Client.GroupV2.CreateGroup(tt.ctx, tt.req)
|
got, err := instance.Client.GroupV2.CreateGroup(tt.ctx, tt.req)
|
||||||
|
afterCreationDate := time.Now().UTC()
|
||||||
if tt.wantErrCode != codes.OK {
|
if tt.wantErrCode != codes.OK {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Empty(t, got.GetId())
|
require.Empty(t, got.GetId())
|
||||||
require.Empty(t, got.GetCreationDate())
|
require.Empty(t, got.GetCreationDate())
|
||||||
assert.Equal(t, tt.wantErrCode, status.Code(err))
|
assert.Equal(t, tt.wantErrCode, status.Code(err))
|
||||||
assert.Equal(t, tt.wantErrMsg, status.Convert(err).Message())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -160,7 +153,7 @@ func TestServer_CreateGroup(t *testing.T) {
|
|||||||
assert.Equal(t, tt.wantGroupID, got.Id, "want: %v, got: %v", tt.wantGroupID, got)
|
assert.Equal(t, tt.wantGroupID, got.Id, "want: %v, got: %v", tt.wantGroupID, got)
|
||||||
}
|
}
|
||||||
require.NotEmpty(t, got.GetId())
|
require.NotEmpty(t, got.GetId())
|
||||||
require.NotEmpty(t, got.GetCreationDate())
|
assert.WithinRange(t, got.GetCreationDate().AsTime(), beforeCreationDate, afterCreationDate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +176,6 @@ func TestServer_UpdateGroup(t *testing.T) {
|
|||||||
req *group_v2.UpdateGroupRequest
|
req *group_v2.UpdateGroupRequest
|
||||||
wantChangeDate bool
|
wantChangeDate bool
|
||||||
wantErrCode codes.Code
|
wantErrCode codes.Code
|
||||||
wantErrMsg string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "unauthenticated, error",
|
name: "unauthenticated, error",
|
||||||
@@ -192,7 +184,6 @@ func TestServer_UpdateGroup(t *testing.T) {
|
|||||||
Name: gu.Ptr(integration.GroupName()),
|
Name: gu.Ptr(integration.GroupName()),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.Unauthenticated,
|
wantErrCode: codes.Unauthenticated,
|
||||||
wantErrMsg: "auth header missing",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid name, error",
|
name: "invalid name, error",
|
||||||
@@ -202,7 +193,6 @@ func TestServer_UpdateGroup(t *testing.T) {
|
|||||||
Name: gu.Ptr(" "),
|
Name: gu.Ptr(" "),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.InvalidArgument,
|
wantErrCode: codes.InvalidArgument,
|
||||||
wantErrMsg: "Errors.Group.InvalidName (GROUP-dUNd3r)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing permission, error",
|
name: "missing permission, error",
|
||||||
@@ -212,7 +202,6 @@ func TestServer_UpdateGroup(t *testing.T) {
|
|||||||
Name: gu.Ptr("updated group name"),
|
Name: gu.Ptr("updated group name"),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.NotFound,
|
wantErrCode: codes.NotFound,
|
||||||
wantErrMsg: "membership not found (AUTHZ-cdgFk)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "organization owner, missing permission, error",
|
name: "organization owner, missing permission, error",
|
||||||
@@ -222,7 +211,6 @@ func TestServer_UpdateGroup(t *testing.T) {
|
|||||||
Name: gu.Ptr("updated group name"),
|
Name: gu.Ptr("updated group name"),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.NotFound,
|
wantErrCode: codes.NotFound,
|
||||||
wantErrMsg: "membership not found (AUTHZ-cdgFk)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "organization owner, with permission, ok",
|
name: "organization owner, with permission, ok",
|
||||||
@@ -241,7 +229,6 @@ func TestServer_UpdateGroup(t *testing.T) {
|
|||||||
Name: gu.Ptr("updated group name 2"),
|
Name: gu.Ptr("updated group name 2"),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.NotFound,
|
wantErrCode: codes.NotFound,
|
||||||
wantErrMsg: "Errors.Group.NotFound (CMDGRP-b33zly)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "instance owner, no change, ok",
|
name: "instance owner, no change, ok",
|
||||||
@@ -250,7 +237,7 @@ func TestServer_UpdateGroup(t *testing.T) {
|
|||||||
Id: existingGroup.GetId(),
|
Id: existingGroup.GetId(),
|
||||||
Name: gu.Ptr(groupName),
|
Name: gu.Ptr(groupName),
|
||||||
},
|
},
|
||||||
wantChangeDate: true,
|
wantChangeDate: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "instance owner, change name, ok",
|
name: "instance owner, change name, ok",
|
||||||
@@ -283,16 +270,19 @@ func TestServer_UpdateGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
beforeUpdateDate := time.Now().UTC()
|
||||||
got, err := instance.Client.GroupV2.UpdateGroup(tt.ctx, tt.req)
|
got, err := instance.Client.GroupV2.UpdateGroup(tt.ctx, tt.req)
|
||||||
|
afterUpdateDate := time.Now().UTC()
|
||||||
if tt.wantErrCode != codes.OK {
|
if tt.wantErrCode != codes.OK {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Empty(t, got.GetChangeDate())
|
require.Empty(t, got.GetChangeDate())
|
||||||
assert.Equal(t, tt.wantErrCode, status.Code(err))
|
assert.Equal(t, tt.wantErrCode, status.Code(err))
|
||||||
assert.Equal(t, tt.wantErrMsg, status.Convert(err).Message())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, got.GetChangeDate())
|
if tt.wantChangeDate {
|
||||||
|
assert.WithinRange(t, got.GetChangeDate().AsTime(), beforeUpdateDate, afterUpdateDate)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,12 +306,12 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
deleteResp := instance.DeleteGroup(iamOwnerCtx, t, deleteGroup.GetId())
|
deleteResp := instance.DeleteGroup(iamOwnerCtx, t, deleteGroup.GetId())
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
req *group_v2.DeleteGroupRequest
|
req *group_v2.DeleteGroupRequest
|
||||||
wantErrCode codes.Code
|
wantErrCode codes.Code
|
||||||
wantErrMsg string
|
wantDeletionDate bool
|
||||||
deletionTime *timestamp.Timestamp
|
deletionTime *timestamp.Timestamp
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "unauthenticated, error",
|
name: "unauthenticated, error",
|
||||||
@@ -330,7 +320,6 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
Id: "12345",
|
Id: "12345",
|
||||||
},
|
},
|
||||||
wantErrCode: codes.Unauthenticated,
|
wantErrCode: codes.Unauthenticated,
|
||||||
wantErrMsg: "auth header missing",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing id, error",
|
name: "missing id, error",
|
||||||
@@ -339,7 +328,6 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
Id: "",
|
Id: "",
|
||||||
},
|
},
|
||||||
wantErrCode: codes.InvalidArgument,
|
wantErrCode: codes.InvalidArgument,
|
||||||
wantErrMsg: "invalid DeleteGroupRequest.Id: value length must be between 1 and 200 runes, inclusive",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing permission, error",
|
name: "missing permission, error",
|
||||||
@@ -348,7 +336,6 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
Id: existingGroup.GetId(),
|
Id: existingGroup.GetId(),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.NotFound,
|
wantErrCode: codes.NotFound,
|
||||||
wantErrMsg: "membership not found (AUTHZ-cdgFk)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "organization owner, missing permission, error",
|
name: "organization owner, missing permission, error",
|
||||||
@@ -357,7 +344,6 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
Id: existingGroup.GetId(),
|
Id: existingGroup.GetId(),
|
||||||
},
|
},
|
||||||
wantErrCode: codes.NotFound,
|
wantErrCode: codes.NotFound,
|
||||||
wantErrMsg: "membership not found (AUTHZ-cdgFk)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "organization owner, with permission, ok",
|
name: "organization owner, with permission, ok",
|
||||||
@@ -365,6 +351,7 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
req: &group_v2.DeleteGroupRequest{
|
req: &group_v2.DeleteGroupRequest{
|
||||||
Id: groupDefOrg.GetId(),
|
Id: groupDefOrg.GetId(),
|
||||||
},
|
},
|
||||||
|
wantDeletionDate: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "group not found, ok",
|
name: "group not found, ok",
|
||||||
@@ -379,6 +366,7 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
req: &group_v2.DeleteGroupRequest{
|
req: &group_v2.DeleteGroupRequest{
|
||||||
Id: existingGroup.GetId(),
|
Id: existingGroup.GetId(),
|
||||||
},
|
},
|
||||||
|
wantDeletionDate: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "delete already deleted group, ok",
|
name: "delete already deleted group, ok",
|
||||||
@@ -391,12 +379,13 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
beforeDeletionDate := time.Now().UTC()
|
||||||
got, err := instance.Client.GroupV2.DeleteGroup(tt.ctx, tt.req)
|
got, err := instance.Client.GroupV2.DeleteGroup(tt.ctx, tt.req)
|
||||||
|
afterDeletionDate := time.Now().UTC()
|
||||||
if tt.wantErrCode != codes.OK {
|
if tt.wantErrCode != codes.OK {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Empty(t, got.GetDeletionDate())
|
require.Empty(t, got.GetDeletionDate())
|
||||||
assert.Equal(t, tt.wantErrCode, status.Code(err))
|
assert.Equal(t, tt.wantErrCode, status.Code(err))
|
||||||
assert.Equal(t, tt.wantErrMsg, status.Convert(err).Message())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -404,6 +393,9 @@ func TestServer_DeleteGroup(t *testing.T) {
|
|||||||
if tt.deletionTime != nil {
|
if tt.deletionTime != nil {
|
||||||
assert.Equal(t, tt.deletionTime, got.GetDeletionDate())
|
assert.Equal(t, tt.deletionTime, got.GetDeletionDate())
|
||||||
}
|
}
|
||||||
|
if tt.wantDeletionDate {
|
||||||
|
assert.WithinRange(t, got.GetDeletionDate().AsTime(), beforeDeletionDate, afterDeletionDate)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
323
internal/api/grpc/group/v2/integration_test/group_users_test.go
Normal file
323
internal/api/grpc/group/v2/integration_test/group_users_test.go
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package group_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
group_v2 "github.com/zitadel/zitadel/pkg/grpc/group/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_AddUsersToGroup(t *testing.T) {
|
||||||
|
iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||||
|
// group and user in the default org
|
||||||
|
defOrgGroup := instance.CreateGroup(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.GroupName())
|
||||||
|
defOrgUser := instance.CreateHumanUserVerified(iamOwnerCtx, instance.DefaultOrg.GetId(), integration.Email(), integration.Phone())
|
||||||
|
|
||||||
|
// org1
|
||||||
|
org1 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
// group in org1
|
||||||
|
group := instance.CreateGroup(iamOwnerCtx, t, org1.GetOrganizationId(), integration.GroupName())
|
||||||
|
// user1 in org1
|
||||||
|
user1 := instance.CreateHumanUserVerified(iamOwnerCtx, org1.OrganizationId, integration.Email(), integration.Phone())
|
||||||
|
// user2 in org1
|
||||||
|
user2 := instance.CreateHumanUserVerified(iamOwnerCtx, org1.OrganizationId, integration.Email(), integration.Phone())
|
||||||
|
// user3 in org1
|
||||||
|
user3 := instance.CreateHumanUserVerified(iamOwnerCtx, org1.OrganizationId, integration.Email(), integration.Phone())
|
||||||
|
// user4 in org1
|
||||||
|
user4 := instance.CreateHumanUserVerified(iamOwnerCtx, org1.OrganizationId, integration.Email(), integration.Phone())
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
req *group_v2.AddUsersToGroupRequest
|
||||||
|
wantChangeDate bool
|
||||||
|
wantErrCode codes.Code
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unauthenticated, error",
|
||||||
|
ctx: context.Background(),
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.Unauthenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing id, error",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing user ids, error",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
},
|
||||||
|
wantErrCode: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group does not exist, error",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: "randomGroup",
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.FailedPrecondition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing permission, error",
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission),
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.NotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "organization owner, missing permission, error",
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.NotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "organization owner, with permission, ok",
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: defOrgGroup.GetId(),
|
||||||
|
UserIds: []string{defOrgUser.GetUserId()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "some users not found, ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId(), "randomUser"},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.FailedPrecondition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user in a different org not added, ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId(), defOrgUser.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.FailedPrecondition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add all users to group, ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantChangeDate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "some users already in the group, ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId(), user3.GetUserId()},
|
||||||
|
},
|
||||||
|
wantChangeDate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add all users to group (with duplicate user IDs), ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user4.GetUserId(), user4.GetUserId()},
|
||||||
|
},
|
||||||
|
wantChangeDate: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
beforeDate := time.Now().UTC()
|
||||||
|
got, err := instance.Client.GroupV2.AddUsersToGroup(tt.ctx, tt.req)
|
||||||
|
afterDate := time.Now().UTC()
|
||||||
|
if tt.wantErrCode != codes.OK {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Empty(t, got.GetChangeDate())
|
||||||
|
assert.Equal(t, tt.wantErrCode, status.Code(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tt.wantChangeDate {
|
||||||
|
assert.WithinRange(t, got.GetChangeDate().AsTime(), beforeDate, afterDate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_RemoveUsersFromGroup(t *testing.T) {
|
||||||
|
iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||||
|
// group and user in the default org
|
||||||
|
defOrgGroup := instance.CreateGroup(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.GroupName())
|
||||||
|
defOrgUser := instance.CreateHumanUserVerified(iamOwnerCtx, instance.DefaultOrg.GetId(), integration.Email(), integration.Phone())
|
||||||
|
|
||||||
|
// add user to the group in def org
|
||||||
|
_, err := instance.Client.GroupV2.AddUsersToGroup(iamOwnerCtx, &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: defOrgGroup.GetId(),
|
||||||
|
UserIds: []string{defOrgUser.GetUserId()},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// org1
|
||||||
|
org1 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
// group in org1
|
||||||
|
group := instance.CreateGroup(iamOwnerCtx, t, org1.GetOrganizationId(), integration.GroupName())
|
||||||
|
// user1 in org1
|
||||||
|
user1 := instance.CreateHumanUserVerified(iamOwnerCtx, org1.OrganizationId, integration.Email(), integration.Phone())
|
||||||
|
// user2 in org1
|
||||||
|
user2 := instance.CreateHumanUserVerified(iamOwnerCtx, org1.OrganizationId, integration.Email(), integration.Phone())
|
||||||
|
// user3 in org1
|
||||||
|
user3 := instance.CreateHumanUserVerified(iamOwnerCtx, org1.OrganizationId, integration.Email(), integration.Phone())
|
||||||
|
|
||||||
|
// add user1, user2, user3 to the group
|
||||||
|
_, err = instance.Client.GroupV2.AddUsersToGroup(iamOwnerCtx, &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId(), user3.GetUserId()},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
req *group_v2.RemoveUsersFromGroupRequest
|
||||||
|
wantChangeDate bool
|
||||||
|
wantErrCode codes.Code
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unauthenticated, error",
|
||||||
|
ctx: context.Background(),
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.Unauthenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing id, error",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing user ids, error",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
},
|
||||||
|
wantErrCode: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group does not exist, error",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: "randomGroup",
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.FailedPrecondition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing permission, error",
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission),
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.NotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "organization owner, missing permission, error",
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.NotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "organization owner, with permission, ok",
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: defOrgGroup.GetId(),
|
||||||
|
UserIds: []string{defOrgUser.GetUserId()},
|
||||||
|
},
|
||||||
|
wantChangeDate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "users not in the group, ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{"user3", "user4"},
|
||||||
|
},
|
||||||
|
wantChangeDate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "some users not in the group, ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user1.GetUserId(), defOrgUser.GetUserId(), "user3"},
|
||||||
|
},
|
||||||
|
wantChangeDate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "users removed, ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user2.GetUserId()},
|
||||||
|
},
|
||||||
|
wantChangeDate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "users removed (with duplicate user IDs), ok",
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.RemoveUsersFromGroupRequest{
|
||||||
|
Id: group.GetId(),
|
||||||
|
UserIds: []string{user3.GetUserId(), user3.GetUserId()},
|
||||||
|
},
|
||||||
|
wantChangeDate: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
beforeDate := time.Now().UTC()
|
||||||
|
got, err := instance.Client.GroupV2.RemoveUsersFromGroup(tt.ctx, tt.req)
|
||||||
|
afterDate := time.Now().UTC()
|
||||||
|
if tt.wantErrCode != codes.OK {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Empty(t, got.GetChangeDate())
|
||||||
|
assert.Equal(t, tt.wantErrCode, status.Code(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tt.wantChangeDate {
|
||||||
|
assert.WithinRange(t, got.GetChangeDate().AsTime(), beforeDate, afterDate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,8 +13,10 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
|
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
|
||||||
group_v2 "github.com/zitadel/zitadel/pkg/grpc/group/v2"
|
group_v2 "github.com/zitadel/zitadel/pkg/grpc/group/v2"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_GetGroup(t *testing.T) {
|
func TestServer_GetGroup(t *testing.T) {
|
||||||
@@ -863,3 +865,648 @@ func TestServer_ListGroups_WithPermissionV2(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_ListGroupUsers(t *testing.T) {
|
||||||
|
iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *group_v2.ListGroupUsersRequest
|
||||||
|
dep func(*group_v2.ListGroupUsersRequest, *group_v2.ListGroupUsersResponse)
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *group_v2.ListGroupUsersResponse
|
||||||
|
wantErrCode codes.Code
|
||||||
|
wantErrMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "list groups, unauthenticated",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &group_v2.ListGroupUsersRequest{},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.Unauthenticated,
|
||||||
|
wantErrMsg: "auth header missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no permission, empty list, TotalResult count returned",
|
||||||
|
args: args{
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission),
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
groupID, _, _ := addUsersToGroup(iamOwnerCtx, t, instance, orgResp.GetOrganizationId(), 2)
|
||||||
|
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{groupID},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 2,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "org owner, missing permission, empty list, TotalResult count returned",
|
||||||
|
args: args{
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
groupID, _, _ := addUsersToGroup(iamOwnerCtx, t, instance, orgResp.GetOrganizationId(), 2)
|
||||||
|
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{groupID},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 2,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "org owner, with permission, ok",
|
||||||
|
args: args{
|
||||||
|
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
groupID, users, addUsersResp := addUsersToGroup(iamOwnerCtx, t, instance, instance.DefaultOrg.GetId(), 2)
|
||||||
|
|
||||||
|
user0, err := instance.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: users[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user1, err := instance.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: users[1]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{groupID},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp.GroupUsers[0] = &group_v2.GroupUser{
|
||||||
|
GroupId: groupID,
|
||||||
|
OrganizationId: instance.DefaultOrg.GetId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: user0.User.GetUserId(),
|
||||||
|
OrganizationId: user0.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: user0.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[1] = &group_v2.GroupUser{
|
||||||
|
GroupId: groupID,
|
||||||
|
OrganizationId: instance.DefaultOrg.GetId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: user1.User.GetUserId(),
|
||||||
|
OrganizationId: instance.DefaultOrg.GetId(),
|
||||||
|
PreferredLoginName: user1.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp.GetChangeDate(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 2,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{
|
||||||
|
{}, {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "instance owner, no matching results, ok",
|
||||||
|
args: args{
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{
|
||||||
|
{
|
||||||
|
Filter: &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{"random-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 0,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list by multiple group IDs, ok",
|
||||||
|
args: args{
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
group0, group0Users, addUsersResp0 := addUsersToGroup(iamOwnerCtx, t, instance, orgResp.GetOrganizationId(), 2)
|
||||||
|
group0User0, err := instance.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group0Users[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
group0User1, err := instance.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group0Users[1]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
group1, group1Users, addUsersResp1 := addUsersToGroup(iamOwnerCtx, t, instance, orgResp.GetOrganizationId(), 2)
|
||||||
|
group1User0, err := instance.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group1Users[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
group1User1, err := instance.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group1Users[1]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp.GroupUsers[0] = &group_v2.GroupUser{
|
||||||
|
GroupId: group1,
|
||||||
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group1User0.User.GetUserId(),
|
||||||
|
OrganizationId: group1User0.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group1User0.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp1.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[1] = &group_v2.GroupUser{
|
||||||
|
GroupId: group1,
|
||||||
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group1User1.User.GetUserId(),
|
||||||
|
OrganizationId: group1User1.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group1User1.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp1.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[2] = &group_v2.GroupUser{
|
||||||
|
GroupId: group0,
|
||||||
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group0User0.User.GetUserId(),
|
||||||
|
OrganizationId: group0User0.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group0User0.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp0.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[3] = &group_v2.GroupUser{
|
||||||
|
GroupId: group0,
|
||||||
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group0User1.User.GetUserId(),
|
||||||
|
OrganizationId: group0User1.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group0User1.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp0.GetChangeDate(),
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{group0, group1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 4,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{
|
||||||
|
{}, {}, {}, {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list by multiple user IDs in user groups from different organizations, ok",
|
||||||
|
args: args{
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
org0 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
group0, group0Users, addUsersResp0 := addUsersToGroup(iamOwnerCtx, t, instance, org0.GetOrganizationId(), 2)
|
||||||
|
group0User0, err := instance.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group0Users[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
org1 := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
group1, group1Users, addUsersResp1 := addUsersToGroup(iamOwnerCtx, t, instance, org1.GetOrganizationId(), 2)
|
||||||
|
group1User1, err := instance.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group1Users[1]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp.GroupUsers[0] = &group_v2.GroupUser{
|
||||||
|
GroupId: group1,
|
||||||
|
OrganizationId: org1.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group1User1.User.GetUserId(),
|
||||||
|
OrganizationId: group1User1.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group1User1.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp1.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[1] = &group_v2.GroupUser{
|
||||||
|
GroupId: group0,
|
||||||
|
OrganizationId: org0.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group0User0.User.GetUserId(),
|
||||||
|
OrganizationId: group0User0.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group0User0.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp0.GetChangeDate(),
|
||||||
|
}
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_UserIds{
|
||||||
|
UserIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{group0User0.User.GetUserId(), group1User1.User.GetUserId()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 2,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{
|
||||||
|
{}, {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.args.dep != nil {
|
||||||
|
tt.args.dep(tt.args.req, tt.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute)
|
||||||
|
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||||
|
got, err := instance.Client.GroupV2.ListGroupUsers(tt.args.ctx, tt.args.req)
|
||||||
|
if tt.wantErrCode != codes.OK {
|
||||||
|
require.Error(ttt, err)
|
||||||
|
assert.Equal(ttt, tt.wantErrCode, status.Code(err))
|
||||||
|
assert.Equal(ttt, tt.wantErrMsg, status.Convert(err).Message())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(ttt, err)
|
||||||
|
if assert.Len(ttt, got.GroupUsers, len(tt.want.GroupUsers)) {
|
||||||
|
for i := range got.GroupUsers {
|
||||||
|
assert.EqualExportedValues(ttt, tt.want.GroupUsers[i], got.GroupUsers[i], "want: %v, got: %v", tt.want.GroupUsers[i], got.GroupUsers[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(ttt, tt.want.Pagination.AppliedLimit, got.Pagination.AppliedLimit)
|
||||||
|
assert.Equal(ttt, tt.want.Pagination.TotalResult, got.Pagination.TotalResult)
|
||||||
|
}, retryDuration, tick, "timeout waiting for expected result")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_ListGroupUsers_WithPermissionV2(t *testing.T) {
|
||||||
|
ensureFeaturePermissionV2Enabled(t, instancePermissionV2)
|
||||||
|
iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *group_v2.ListGroupUsersRequest
|
||||||
|
dep func(*group_v2.ListGroupUsersRequest, *group_v2.ListGroupUsersResponse)
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *group_v2.ListGroupUsersResponse
|
||||||
|
wantErrCode codes.Code
|
||||||
|
wantErrMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "list groups, unauthenticated",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &group_v2.ListGroupUsersRequest{},
|
||||||
|
},
|
||||||
|
wantErrCode: codes.Unauthenticated,
|
||||||
|
wantErrMsg: "auth header missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no permission, empty list, TotalResult count set to 0",
|
||||||
|
args: args{
|
||||||
|
ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission),
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
groupID, _, _ := addUsersToGroup(iamOwnerCtx, t, instancePermissionV2, orgResp.GetOrganizationId(), 2)
|
||||||
|
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{groupID},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 0,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "org owner, missing permission, empty list, TotalResult count set to 0",
|
||||||
|
args: args{
|
||||||
|
ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
groupID, _, _ := addUsersToGroup(iamOwnerCtx, t, instancePermissionV2, orgResp.GetOrganizationId(), 2)
|
||||||
|
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{groupID},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 0,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "org owner, with permission, ok",
|
||||||
|
args: args{
|
||||||
|
ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
groupID, users, addUsersResp := addUsersToGroup(iamOwnerCtx, t, instancePermissionV2, instancePermissionV2.DefaultOrg.GetId(), 2)
|
||||||
|
|
||||||
|
user0, err := instancePermissionV2.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: users[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user1, err := instancePermissionV2.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: users[1]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{groupID},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp.GroupUsers[0] = &group_v2.GroupUser{
|
||||||
|
GroupId: groupID,
|
||||||
|
OrganizationId: instancePermissionV2.DefaultOrg.GetId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: user0.User.GetUserId(),
|
||||||
|
OrganizationId: user0.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: user0.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[1] = &group_v2.GroupUser{
|
||||||
|
GroupId: groupID,
|
||||||
|
OrganizationId: instancePermissionV2.DefaultOrg.GetId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: user1.User.GetUserId(),
|
||||||
|
OrganizationId: instancePermissionV2.DefaultOrg.GetId(),
|
||||||
|
PreferredLoginName: user1.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp.GetChangeDate(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 2,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{
|
||||||
|
{}, {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "instance owner, no matching results, ok",
|
||||||
|
args: args{
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{
|
||||||
|
{
|
||||||
|
Filter: &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{"random-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 0,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list by multiple group IDs, ok",
|
||||||
|
args: args{
|
||||||
|
ctx: iamOwnerCtx,
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
group0, group0Users, addUsersResp0 := addUsersToGroup(iamOwnerCtx, t, instancePermissionV2, orgResp.GetOrganizationId(), 2)
|
||||||
|
group0User0, err := instancePermissionV2.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group0Users[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
group0User1, err := instancePermissionV2.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group0Users[1]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
group1, group1Users, addUsersResp1 := addUsersToGroup(iamOwnerCtx, t, instancePermissionV2, orgResp.GetOrganizationId(), 2)
|
||||||
|
group1User0, err := instancePermissionV2.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group1Users[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
group1User1, err := instancePermissionV2.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group1Users[1]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp.GroupUsers[0] = &group_v2.GroupUser{
|
||||||
|
GroupId: group1,
|
||||||
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group1User0.User.GetUserId(),
|
||||||
|
OrganizationId: group1User0.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group1User0.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp1.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[1] = &group_v2.GroupUser{
|
||||||
|
GroupId: group1,
|
||||||
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group1User1.User.GetUserId(),
|
||||||
|
OrganizationId: group1User1.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group1User1.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp1.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[2] = &group_v2.GroupUser{
|
||||||
|
GroupId: group0,
|
||||||
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group0User0.User.GetUserId(),
|
||||||
|
OrganizationId: group0User0.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group0User0.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp0.GetChangeDate(),
|
||||||
|
}
|
||||||
|
resp.GroupUsers[3] = &group_v2.GroupUser{
|
||||||
|
GroupId: group0,
|
||||||
|
OrganizationId: orgResp.GetOrganizationId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group0User1.User.GetUserId(),
|
||||||
|
OrganizationId: group0User1.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group0User1.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp0.GetChangeDate(),
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{group0, group1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 4,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{
|
||||||
|
{}, {}, {}, {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list by multiple user IDs in user groups from different organizations with permission in one org, ok",
|
||||||
|
args: args{
|
||||||
|
ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||||
|
dep: func(req *group_v2.ListGroupUsersRequest, resp *group_v2.ListGroupUsersResponse) {
|
||||||
|
group0, group0Users, addUsersResp0 := addUsersToGroup(iamOwnerCtx, t, instancePermissionV2, instancePermissionV2.DefaultOrg.GetId(), 2)
|
||||||
|
group0User0, err := instancePermissionV2.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group0Users[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
org1 := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
|
||||||
|
_, group1Users, _ := addUsersToGroup(iamOwnerCtx, t, instancePermissionV2, org1.GetOrganizationId(), 2)
|
||||||
|
group1User1, err := instancePermissionV2.Client.UserV2.GetUserByID(iamOwnerCtx, &user.GetUserByIDRequest{UserId: group1Users[1]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp.GroupUsers[0] = &group_v2.GroupUser{
|
||||||
|
GroupId: group0,
|
||||||
|
OrganizationId: instancePermissionV2.DefaultOrg.GetId(),
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: group0User0.User.GetUserId(),
|
||||||
|
OrganizationId: group0User0.User.Details.ResourceOwner,
|
||||||
|
PreferredLoginName: group0User0.User.GetPreferredLoginName(),
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
},
|
||||||
|
CreationDate: addUsersResp0.GetChangeDate(),
|
||||||
|
}
|
||||||
|
req.Filters[0].Filter = &group_v2.GroupUsersSearchFilter_UserIds{
|
||||||
|
UserIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{group0User0.User.GetUserId(), group1User1.User.GetUserId()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &group_v2.ListGroupUsersResponse{
|
||||||
|
Pagination: &filter.PaginationResponse{
|
||||||
|
TotalResult: 1,
|
||||||
|
AppliedLimit: 100,
|
||||||
|
},
|
||||||
|
GroupUsers: []*group_v2.GroupUser{
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.args.dep != nil {
|
||||||
|
tt.args.dep(tt.args.req, tt.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute)
|
||||||
|
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||||
|
got, err := instancePermissionV2.Client.GroupV2.ListGroupUsers(tt.args.ctx, tt.args.req)
|
||||||
|
if tt.wantErrCode != codes.OK {
|
||||||
|
require.Error(ttt, err)
|
||||||
|
assert.Equal(ttt, tt.wantErrCode, status.Code(err))
|
||||||
|
assert.Equal(ttt, tt.wantErrMsg, status.Convert(err).Message())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(ttt, err)
|
||||||
|
if assert.Len(ttt, got.GroupUsers, len(tt.want.GroupUsers)) {
|
||||||
|
for i := range got.GroupUsers {
|
||||||
|
assert.EqualExportedValues(ttt, tt.want.GroupUsers[i], got.GroupUsers[i], "want: %v, got: %v", tt.want.GroupUsers[i], got.GroupUsers[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(ttt, tt.want.Pagination.AppliedLimit, got.Pagination.AppliedLimit)
|
||||||
|
assert.Equal(ttt, tt.want.Pagination.TotalResult, got.Pagination.TotalResult)
|
||||||
|
}, retryDuration, tick, "timeout waiting for expected result")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUsersToGroup(ctx context.Context, t *testing.T, instance *integration.Instance, orgID string, numUsers int) (string, []string, *group_v2.AddUsersToGroupResponse) {
|
||||||
|
groupName := integration.GroupName()
|
||||||
|
group1 := instance.CreateGroup(ctx, t, orgID, groupName)
|
||||||
|
users := make([]string, 0, numUsers)
|
||||||
|
for i := 0; i < numUsers; i++ {
|
||||||
|
u := instance.CreateHumanUserVerified(ctx, orgID, integration.Email(), integration.Phone())
|
||||||
|
users = append(users, u.GetUserId())
|
||||||
|
}
|
||||||
|
addUsersResp := instance.AddUsersToGroup(ctx, t, group1.GetId(), users)
|
||||||
|
return group1.GetId(), users, addUsersResp
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
authorization_v2beta "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
|
||||||
group_v2 "github.com/zitadel/zitadel/pkg/grpc/group/v2"
|
group_v2 "github.com/zitadel/zitadel/pkg/grpc/group/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,6 +41,22 @@ func (s *Server) ListGroups(ctx context.Context, req *connect.Request[group_v2.L
|
|||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListGroupUsers returns a list of users from user groupUsers that match the search criteria
|
||||||
|
func (s *Server) ListGroupUsers(ctx context.Context, req *connect.Request[group_v2.ListGroupUsersRequest]) (*connect.Response[group_v2.ListGroupUsersResponse], error) {
|
||||||
|
queries, err := listGroupUsersRequestToModel(req.Msg, s.systemDefaults)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := s.query.SearchGroupUsers(ctx, queries, s.checkPermission)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return connect.NewResponse(&group_v2.ListGroupUsersResponse{
|
||||||
|
GroupUsers: groupUsersToPb(resp.GroupUsers),
|
||||||
|
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
func listGroupsRequestToModel(req *group_v2.ListGroupsRequest, systemDefaults systemdefaults.SystemDefaults) (*query.GroupSearchQuery, error) {
|
func listGroupsRequestToModel(req *group_v2.ListGroupsRequest, systemDefaults systemdefaults.SystemDefaults) (*query.GroupSearchQuery, error) {
|
||||||
offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, req.GetPagination())
|
offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, req.GetPagination())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,3 +137,85 @@ func groupToPb(g *query.Group) *group_v2.Group {
|
|||||||
ChangeDate: timestamppb.New(g.ChangeDate),
|
ChangeDate: timestamppb.New(g.ChangeDate),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listGroupUsersRequestToModel(req *group_v2.ListGroupUsersRequest, systemDefaults systemdefaults.SystemDefaults) (*query.GroupUsersSearchQuery, error) {
|
||||||
|
offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, req.GetPagination())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
queries, err := groupUsersSearchFiltersToQuery(req.GetFilters())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &query.GroupUsersSearchQuery{
|
||||||
|
SearchRequest: query.SearchRequest{
|
||||||
|
Offset: offset,
|
||||||
|
Limit: limit,
|
||||||
|
Asc: asc,
|
||||||
|
SortingColumn: groupUsersFieldNameToSortingColumn(req.SortingColumn),
|
||||||
|
},
|
||||||
|
Queries: queries,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupUsersSearchFiltersToQuery(filters []*group_v2.GroupUsersSearchFilter) (_ []query.SearchQuery, err error) {
|
||||||
|
q := make([]query.SearchQuery, len(filters))
|
||||||
|
for i, f := range filters {
|
||||||
|
q[i], err = groupUsersFilterToQuery(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return q, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupUsersFilterToQuery(f *group_v2.GroupUsersSearchFilter) (query.SearchQuery, error) {
|
||||||
|
switch q := f.Filter.(type) {
|
||||||
|
case *group_v2.GroupUsersSearchFilter_UserIds:
|
||||||
|
return query.NewGroupUsersUserIDsSearchQuery(q.UserIds.GetIds())
|
||||||
|
case *group_v2.GroupUsersSearchFilter_GroupIds:
|
||||||
|
return query.NewGroupUsersGroupIDsSearchQuery(q.GroupIds.GetIds())
|
||||||
|
default:
|
||||||
|
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-osMHhx", "List.Query.Invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupUsersFieldNameToSortingColumn(field *group_v2.GroupUserFieldName) query.Column {
|
||||||
|
if field == nil {
|
||||||
|
return query.GroupUsersColumnCreationDate
|
||||||
|
}
|
||||||
|
switch *field {
|
||||||
|
case group_v2.GroupUserFieldName_GROUP_USER_FIELD_NAME_CREATION_DATE,
|
||||||
|
group_v2.GroupUserFieldName_GROUP_USER_FIELD_NAME_UNSPECIFIED:
|
||||||
|
return query.GroupUsersColumnCreationDate
|
||||||
|
case group_v2.GroupUserFieldName_GROUP_USER_FIELD_NAME_USER_ID:
|
||||||
|
return query.GroupUsersColumnUserID
|
||||||
|
case group_v2.GroupUserFieldName_GROUP_USER_FIELD_NAME_GROUP_ID:
|
||||||
|
return query.GroupUsersColumnGroupID
|
||||||
|
default:
|
||||||
|
return query.GroupUsersColumnCreationDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupUsersToPb(groupUsers []*query.GroupUser) []*group_v2.GroupUser {
|
||||||
|
pbGroupUsers := make([]*group_v2.GroupUser, len(groupUsers))
|
||||||
|
for i, gu := range groupUsers {
|
||||||
|
pbGroupUsers[i] = groupUserToPb(gu)
|
||||||
|
}
|
||||||
|
return pbGroupUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupUserToPb(gu *query.GroupUser) *group_v2.GroupUser {
|
||||||
|
return &group_v2.GroupUser{
|
||||||
|
GroupId: gu.GroupID,
|
||||||
|
OrganizationId: gu.ResourceOwner,
|
||||||
|
User: &authorization_v2beta.User{
|
||||||
|
Id: gu.UserID,
|
||||||
|
PreferredLoginName: gu.PreferredLoginName,
|
||||||
|
DisplayName: gu.DisplayName,
|
||||||
|
AvatarUrl: gu.AvatarUrl,
|
||||||
|
OrganizationId: gu.ResourceOwner,
|
||||||
|
},
|
||||||
|
CreationDate: timestamppb.New(gu.CreationDate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package group
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,12 +14,13 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
|
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
|
||||||
group_v2 "github.com/zitadel/zitadel/pkg/grpc/group/v2"
|
group_v2 "github.com/zitadel/zitadel/pkg/grpc/group/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_ListGroupsRequestToModel(t *testing.T) {
|
func Test_ListGroupsRequestToModel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
groupIDsSearchQuery, err := query.NewGroupIDsSearchQuery([]string{"group1", "group2"})
|
groupIDsSearchQuery, err := query.NewGroupIDsSearchQuery([]string{"group1", "group2"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -76,16 +76,13 @@ func Test_ListGroupsRequestToModel(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit}
|
sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit}
|
||||||
got, err := listGroupsRequestToModel(tt.req, sysDefaults)
|
got, err := listGroupsRequestToModel(tt.req, sysDefaults)
|
||||||
if tt.wantErr != nil {
|
if tt.wantErr != nil {
|
||||||
assert.Equal(t, tt.wantErr, err)
|
assert.Equal(t, tt.wantErr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, q := range got.Queries {
|
|
||||||
fmt.Printf("%+v", q)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, tt.wantResp, got)
|
assert.Equal(t, tt.wantResp, got)
|
||||||
})
|
})
|
||||||
@@ -93,6 +90,7 @@ func Test_ListGroupsRequestToModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_GroupSearchFiltersToQuery(t *testing.T) {
|
func Test_GroupSearchFiltersToQuery(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
groupIDsSearchQuery, err := query.NewGroupIDsSearchQuery([]string{"group1", "group2"})
|
groupIDsSearchQuery, err := query.NewGroupIDsSearchQuery([]string{"group1", "group2"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
groupNameSearchQuery, err := query.NewGroupNameSearchQuery("mygroup", query.TextStartsWith)
|
groupNameSearchQuery, err := query.NewGroupNameSearchQuery("mygroup", query.TextStartsWith)
|
||||||
@@ -146,6 +144,7 @@ func Test_GroupSearchFiltersToQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
got, err := groupSearchFiltersToQuery(tt.filters)
|
got, err := groupSearchFiltersToQuery(tt.filters)
|
||||||
if tt.wantErr != nil {
|
if tt.wantErr != nil {
|
||||||
assert.Equal(t, tt.wantErr, err)
|
assert.Equal(t, tt.wantErr, err)
|
||||||
@@ -158,6 +157,7 @@ func Test_GroupSearchFiltersToQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_GroupFieldNameToSortingColumn(t *testing.T) {
|
func Test_GroupFieldNameToSortingColumn(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
field *group_v2.FieldName
|
field *group_v2.FieldName
|
||||||
@@ -196,6 +196,7 @@ func Test_GroupFieldNameToSortingColumn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
got := groupFieldNameToSortingColumn(tt.field)
|
got := groupFieldNameToSortingColumn(tt.field)
|
||||||
assert.Equal(t, tt.want, got)
|
assert.Equal(t, tt.want, got)
|
||||||
})
|
})
|
||||||
@@ -203,6 +204,7 @@ func Test_GroupFieldNameToSortingColumn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_GroupsToPb(t *testing.T) {
|
func Test_GroupsToPb(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
timeNow := time.Now().UTC()
|
timeNow := time.Now().UTC()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -262,8 +264,299 @@ func Test_GroupsToPb(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
got := groupsToPb(tt.groups)
|
got := groupsToPb(tt.groups)
|
||||||
assert.Equal(t, tt.want, got)
|
assert.Equal(t, tt.want, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_ListGroupUsersRequestToModel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
groupIDsSearchQuery, err := query.NewGroupUsersGroupIDsSearchQuery([]string{"group1", "group2"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userIDsSearchQuery, err := query.NewGroupUsersUserIDsSearchQuery([]string{"user1", "user2"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
maxQueryLimit uint64
|
||||||
|
req *group_v2.ListGroupUsersRequest
|
||||||
|
wantResp *query.GroupUsersSearchQuery
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "max query limit exceeded",
|
||||||
|
maxQueryLimit: 1,
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Pagination: &filter.PaginationRequest{
|
||||||
|
Limit: 5,
|
||||||
|
},
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{
|
||||||
|
{
|
||||||
|
Filter: &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{"group1", "group2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: zerrors.ThrowInvalidArgumentf(errors.New("given: 5, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid request, list of group IDs, ok",
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{
|
||||||
|
{
|
||||||
|
Filter: &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{"group1", "group2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantResp: &query.GroupUsersSearchQuery{
|
||||||
|
SearchRequest: query.SearchRequest{
|
||||||
|
Offset: 0,
|
||||||
|
Limit: 0,
|
||||||
|
SortingColumn: query.GroupUsersColumnCreationDate,
|
||||||
|
},
|
||||||
|
Queries: []query.SearchQuery{groupIDsSearchQuery},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid request, list of user IDs, ok",
|
||||||
|
req: &group_v2.ListGroupUsersRequest{
|
||||||
|
Filters: []*group_v2.GroupUsersSearchFilter{
|
||||||
|
{
|
||||||
|
Filter: &group_v2.GroupUsersSearchFilter_UserIds{
|
||||||
|
UserIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantResp: &query.GroupUsersSearchQuery{
|
||||||
|
SearchRequest: query.SearchRequest{
|
||||||
|
Offset: 0,
|
||||||
|
Limit: 0,
|
||||||
|
SortingColumn: query.GroupUsersColumnCreationDate,
|
||||||
|
},
|
||||||
|
Queries: []query.SearchQuery{userIDsSearchQuery},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit}
|
||||||
|
got, err := listGroupUsersRequestToModel(tt.req, sysDefaults)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
assert.Equal(t, tt.wantErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.wantResp, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GroupUsersSearchFiltersToQuery(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
groupIDsSearchQuery, err := query.NewGroupUsersGroupIDsSearchQuery([]string{"group1", "group2"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userIDsSearchQuery, err := query.NewGroupUsersUserIDsSearchQuery([]string{"user1", "user2"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
filters []*group_v2.GroupUsersSearchFilter
|
||||||
|
want []query.SearchQuery
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
filters: []*group_v2.GroupUsersSearchFilter{},
|
||||||
|
want: []query.SearchQuery{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all filters",
|
||||||
|
filters: []*group_v2.GroupUsersSearchFilter{
|
||||||
|
{
|
||||||
|
Filter: &group_v2.GroupUsersSearchFilter_GroupIds{
|
||||||
|
GroupIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{"group1", "group2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Filter: &group_v2.GroupUsersSearchFilter_UserIds{
|
||||||
|
UserIds: &filter.InIDsFilter{
|
||||||
|
Ids: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []query.SearchQuery{
|
||||||
|
groupIDsSearchQuery,
|
||||||
|
userIDsSearchQuery,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got, err := groupUsersSearchFiltersToQuery(tt.filters)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
assert.Equal(t, tt.wantErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GroupUsersFieldNameToSortingColumn(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
field *group_v2.GroupUserFieldName
|
||||||
|
want query.Column
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
field: nil,
|
||||||
|
want: query.GroupUsersColumnCreationDate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "creation date",
|
||||||
|
field: gu.Ptr(group_v2.GroupUserFieldName_GROUP_USER_FIELD_NAME_CREATION_DATE),
|
||||||
|
want: query.GroupUsersColumnCreationDate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unspecified",
|
||||||
|
field: gu.Ptr(group_v2.GroupUserFieldName_GROUP_USER_FIELD_NAME_CREATION_DATE),
|
||||||
|
want: query.GroupUsersColumnCreationDate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group id",
|
||||||
|
field: gu.Ptr(group_v2.GroupUserFieldName_GROUP_USER_FIELD_NAME_GROUP_ID),
|
||||||
|
want: query.GroupUsersColumnGroupID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user id",
|
||||||
|
field: gu.Ptr(group_v2.GroupUserFieldName_GROUP_USER_FIELD_NAME_USER_ID),
|
||||||
|
want: query.GroupUsersColumnUserID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got := groupUsersFieldNameToSortingColumn(tt.field)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GroupUsersToPb(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
timeNow := time.Now().UTC()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
groupUsers []*query.GroupUser
|
||||||
|
want []*group_v2.GroupUser
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
groupUsers: []*query.GroupUser{},
|
||||||
|
want: []*group_v2.GroupUser{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with groupUsers, ok",
|
||||||
|
groupUsers: []*query.GroupUser{
|
||||||
|
{
|
||||||
|
GroupID: "group1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
CreationDate: timeNow,
|
||||||
|
Sequence: 1,
|
||||||
|
UserID: "user1",
|
||||||
|
PreferredLoginName: "user1",
|
||||||
|
DisplayName: "user1",
|
||||||
|
AvatarUrl: "example.com/user1.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupID: "group1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
CreationDate: timeNow,
|
||||||
|
Sequence: 1,
|
||||||
|
UserID: "user2",
|
||||||
|
PreferredLoginName: "user2",
|
||||||
|
DisplayName: "user2",
|
||||||
|
AvatarUrl: "example.com/user2.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupID: "group2",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
CreationDate: timeNow,
|
||||||
|
Sequence: 1,
|
||||||
|
UserID: "user1",
|
||||||
|
PreferredLoginName: "user1",
|
||||||
|
DisplayName: "user1",
|
||||||
|
AvatarUrl: "example.com/user1.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*group_v2.GroupUser{
|
||||||
|
{
|
||||||
|
GroupId: "group1",
|
||||||
|
OrganizationId: "org1",
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: "user1",
|
||||||
|
DisplayName: "user1",
|
||||||
|
PreferredLoginName: "user1",
|
||||||
|
AvatarUrl: "example.com/user1.png",
|
||||||
|
OrganizationId: "org1",
|
||||||
|
},
|
||||||
|
CreationDate: timestamppb.New(timeNow),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupId: "group1",
|
||||||
|
OrganizationId: "org1",
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: "user2",
|
||||||
|
DisplayName: "user2",
|
||||||
|
PreferredLoginName: "user2",
|
||||||
|
AvatarUrl: "example.com/user2.png",
|
||||||
|
OrganizationId: "org1",
|
||||||
|
},
|
||||||
|
CreationDate: timestamppb.New(timeNow),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupId: "group2",
|
||||||
|
OrganizationId: "org1",
|
||||||
|
User: &authorization.User{
|
||||||
|
Id: "user1",
|
||||||
|
DisplayName: "user1",
|
||||||
|
PreferredLoginName: "user1",
|
||||||
|
AvatarUrl: "example.com/user1.png",
|
||||||
|
OrganizationId: "org1",
|
||||||
|
},
|
||||||
|
CreationDate: timestamppb.New(timeNow),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got := groupUsersToPb(tt.groupUsers)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -349,11 +349,11 @@ func (s *Server) UnlockUser(ctx context.Context, req *mgmt_pb.UnlockUserRequest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) RemoveUser(ctx context.Context, req *mgmt_pb.RemoveUserRequest) (*mgmt_pb.RemoveUserResponse, error) {
|
func (s *Server) RemoveUser(ctx context.Context, req *mgmt_pb.RemoveUserRequest) (*mgmt_pb.RemoveUserResponse, error) {
|
||||||
memberships, grants, err := s.removeUserDependencies(ctx, req.Id)
|
memberships, grants, groupIDs, err := s.removeUserDependencies(ctx, req.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
objectDetails, err := s.command.RemoveUser(ctx, req.Id, authz.GetCtxData(ctx).OrgID, memberships, grants...)
|
objectDetails, err := s.command.RemoveUser(ctx, req.Id, authz.GetCtxData(ctx).OrgID, memberships, grants, groupIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -362,28 +362,32 @@ func (s *Server) RemoveUser(ctx context.Context, req *mgmt_pb.RemoveUserRequest)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, []string, error) {
|
||||||
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
||||||
Queries: []query.SearchQuery{userGrantUserQuery},
|
Queries: []query.SearchQuery{userGrantUserQuery},
|
||||||
}, true, nil)
|
}, true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{
|
memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{
|
||||||
Queries: []query.SearchQuery{membershipsUserQuery},
|
Queries: []query.SearchQuery{membershipsUserQuery},
|
||||||
}, false)
|
}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), nil
|
groupIDs, err := s.getGroupsByUserID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), groupIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) UpdateUserName(ctx context.Context, req *mgmt_pb.UpdateUserNameRequest) (*mgmt_pb.UpdateUserNameResponse, error) {
|
func (s *Server) UpdateUserName(ctx context.Context, req *mgmt_pb.UpdateUserNameRequest) (*mgmt_pb.UpdateUserNameResponse, error) {
|
||||||
@@ -977,3 +981,27 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string {
|
|||||||
}
|
}
|
||||||
return converted
|
return converted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) getGroupsByUserID(ctx context.Context, userID string) ([]string, error) {
|
||||||
|
groupUserQuery, err := query.NewGroupUsersUserIDsSearchQuery([]string{userID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groupUsers, err := s.query.SearchGroupUsers(ctx,
|
||||||
|
&query.GroupUsersSearchQuery{
|
||||||
|
Queries: []query.SearchQuery{
|
||||||
|
groupUserQuery,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(groupUsers.GroupUsers) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
groupIDs := make([]string, 0, len(groupUsers.GroupUsers))
|
||||||
|
for _, groupUser := range groupUsers.GroupUsers {
|
||||||
|
groupIDs = append(groupIDs, groupUser.GroupID)
|
||||||
|
}
|
||||||
|
return groupIDs, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,11 +184,11 @@ func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.DeleteUserRequest]) (_ *connect.Response[user.DeleteUserResponse], err error) {
|
func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.DeleteUserRequest]) (_ *connect.Response[user.DeleteUserResponse], err error) {
|
||||||
memberships, grants, err := s.removeUserDependencies(ctx, req.Msg.GetUserId())
|
memberships, grants, groupIDs, err := s.removeUserDependencies(ctx, req.Msg.GetUserId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
details, err := s.command.RemoveUserV2(ctx, req.Msg.GetUserId(), "", memberships, grants...)
|
details, err := s.command.RemoveUserV2(ctx, req.Msg.GetUserId(), "", memberships, grants, groupIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -197,28 +197,32 @@ func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.Delet
|
|||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, []string, error) {
|
||||||
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
||||||
Queries: []query.SearchQuery{userGrantUserQuery},
|
Queries: []query.SearchQuery{userGrantUserQuery},
|
||||||
}, true, nil)
|
}, true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{
|
memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{
|
||||||
Queries: []query.SearchQuery{membershipsUserQuery},
|
Queries: []query.SearchQuery{membershipsUserQuery},
|
||||||
}, false)
|
}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), nil
|
groupIDs, err := s.getGroupsByUserID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), groupIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership {
|
func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership {
|
||||||
@@ -426,3 +430,27 @@ func (s *Server) UpdateUser(ctx context.Context, req *connect.Request[user.Updat
|
|||||||
return nil, zerrors.ThrowUnimplemented(nil, "", "user type is not implemented")
|
return nil, zerrors.ThrowUnimplemented(nil, "", "user type is not implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) getGroupsByUserID(ctx context.Context, userID string) ([]string, error) {
|
||||||
|
groupUserQuery, err := query.NewGroupUsersUserIDsSearchQuery([]string{userID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groupUsers, err := s.query.SearchGroupUsers(ctx,
|
||||||
|
&query.GroupUsersSearchQuery{
|
||||||
|
Queries: []query.SearchQuery{
|
||||||
|
groupUserQuery,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(groupUsers.GroupUsers) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
groupIDs := make([]string, 0, len(groupUsers.GroupUsers))
|
||||||
|
for _, groupUser := range groupUsers.GroupUsers {
|
||||||
|
groupIDs = append(groupIDs, groupUser.GroupID)
|
||||||
|
}
|
||||||
|
return groupIDs, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -276,11 +276,11 @@ func (s *Server) AddIDPLink(ctx context.Context, req *connect.Request[user.AddID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.DeleteUserRequest]) (_ *connect.Response[user.DeleteUserResponse], err error) {
|
func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.DeleteUserRequest]) (_ *connect.Response[user.DeleteUserResponse], err error) {
|
||||||
memberships, grants, err := s.removeUserDependencies(ctx, req.Msg.GetUserId())
|
memberships, grants, groupIDs, err := s.removeUserDependencies(ctx, req.Msg.GetUserId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
details, err := s.command.RemoveUserV2(ctx, req.Msg.GetUserId(), "", memberships, grants...)
|
details, err := s.command.RemoveUserV2(ctx, req.Msg.GetUserId(), "", memberships, grants, groupIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -289,28 +289,32 @@ func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.Delet
|
|||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, []string, error) {
|
||||||
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
||||||
Queries: []query.SearchQuery{userGrantUserQuery},
|
Queries: []query.SearchQuery{userGrantUserQuery},
|
||||||
}, true, nil)
|
}, true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{
|
memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{
|
||||||
Queries: []query.SearchQuery{membershipsUserQuery},
|
Queries: []query.SearchQuery{membershipsUserQuery},
|
||||||
}, false)
|
}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), nil
|
groupIDs, err := s.getGroupsByUserID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), groupIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership {
|
func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership {
|
||||||
@@ -645,3 +649,27 @@ func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.Authenticatio
|
|||||||
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) getGroupsByUserID(ctx context.Context, userID string) ([]string, error) {
|
||||||
|
groupUserQuery, err := query.NewGroupUsersUserIDsSearchQuery([]string{userID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groupUsers, err := s.query.SearchGroupUsers(ctx,
|
||||||
|
&query.GroupUsersSearchQuery{
|
||||||
|
Queries: []query.SearchQuery{
|
||||||
|
groupUserQuery,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(groupUsers.GroupUsers) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
groupIDs := make([]string, 0, len(groupUsers.GroupUsers))
|
||||||
|
for _, groupUser := range groupUsers.GroupUsers {
|
||||||
|
groupIDs = append(groupIDs, groupUser.GroupID)
|
||||||
|
}
|
||||||
|
return groupIDs, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -200,11 +200,11 @@ func (h *UsersHandler) Update(ctx context.Context, id string, operations patch.O
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) Delete(ctx context.Context, id string) error {
|
func (h *UsersHandler) Delete(ctx context.Context, id string) error {
|
||||||
memberships, grants, err := h.queryUserDependencies(ctx, id)
|
memberships, grants, groupIDs, err := h.queryUserDependencies(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = h.command.RemoveUserV2(ctx, id, authz.GetCtxData(ctx).OrgID, memberships, grants...)
|
_, err = h.command.RemoveUserV2(ctx, id, authz.GetCtxData(ctx).OrgID, memberships, grants, groupIDs)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,22 +254,22 @@ func (h *UsersHandler) List(ctx context.Context, request *ListRequest) (*ListRes
|
|||||||
return NewListResponse(users.SearchResponse.Count, q.SearchRequest, scimUsers), nil
|
return NewListResponse(users.SearchResponse.Count, q.SearchRequest, scimUsers), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, []string, error) {
|
||||||
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
grants, err := h.query.UserGrants(ctx, &query.UserGrantsQueries{
|
grants, err := h.query.UserGrants(ctx, &query.UserGrantsQueries{
|
||||||
Queries: []query.SearchQuery{userGrantUserQuery},
|
Queries: []query.SearchQuery{userGrantUserQuery},
|
||||||
}, true, nil)
|
}, true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
memberships, err := h.query.Memberships(ctx, &query.MembershipSearchQuery{
|
memberships, err := h.query.Memberships(ctx, &query.MembershipSearchQuery{
|
||||||
@@ -277,7 +277,35 @@ func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string)
|
|||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), nil
|
groupIDs, err := h.getGroupsByUserID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), groupIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UsersHandler) getGroupsByUserID(ctx context.Context, userID string) ([]string, error) {
|
||||||
|
groupUserQuery, err := query.NewGroupUsersUserIDsSearchQuery([]string{userID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groupUsers, err := h.query.SearchGroupUsers(ctx,
|
||||||
|
&query.GroupUsersSearchQuery{
|
||||||
|
Queries: []query.SearchQuery{
|
||||||
|
groupUserQuery,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(groupUsers.GroupUsers) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
groupIDs := make([]string, 0, len(groupUsers.GroupUsers))
|
||||||
|
for _, groupUser := range groupUsers.GroupUsers {
|
||||||
|
groupIDs = append(groupIDs, groupUser.GroupID)
|
||||||
|
}
|
||||||
|
return groupIDs, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func (c *Commands) CreateGroup(ctx context.Context, group *CreateGroup) (details
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if a group with the same ID already exists
|
// check if a group with the same ID already exists
|
||||||
groupWriteModel, err := c.getGroupWriteModelByID(ctx, group.AggregateID, group.ResourceOwner)
|
groupWriteModel, err := c.getGroupWriteModelByID(ctx, group.AggregateID, group.ResourceOwner, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ func (c *Commands) UpdateGroup(ctx context.Context, groupUpdate *UpdateGroup) (d
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingGroup, err := c.getGroupWriteModelByID(ctx, groupUpdate.AggregateID, groupUpdate.ResourceOwner)
|
existingGroup, err := c.getGroupWriteModelByID(ctx, groupUpdate.AggregateID, groupUpdate.ResourceOwner, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ func (c *Commands) DeleteGroup(ctx context.Context, groupID string) (details *do
|
|||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
existingGroup, err := c.getGroupWriteModelByID(ctx, groupID, "")
|
existingGroup, err := c.getGroupWriteModelByID(ctx, groupID, "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -160,11 +160,22 @@ func (c *Commands) DeleteGroup(ctx context.Context, groupID string) (details *do
|
|||||||
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
|
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) getGroupWriteModelByID(ctx context.Context, id, orgID string) (*GroupWriteModel, error) {
|
func (c *Commands) getGroupWriteModelByID(ctx context.Context, groupID, orgID string, userIDs []string) (*GroupWriteModel, error) {
|
||||||
groupWriteModel := NewGroupWriteModel(id, orgID)
|
groupWriteModel := NewGroupWriteModel(groupID, orgID, userIDs)
|
||||||
err := c.eventstore.FilterToQueryReducer(ctx, groupWriteModel)
|
err := c.eventstore.FilterToQueryReducer(ctx, groupWriteModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return groupWriteModel, nil
|
return groupWriteModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Commands) checkGroupExists(ctx context.Context, groupID string, userIDs []string) (*GroupWriteModel, error) {
|
||||||
|
group, err := c.getGroupWriteModelByID(ctx, groupID, "", userIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !group.State.Exists() {
|
||||||
|
return nil, zerrors.ThrowPreconditionFailed(nil, "CMDGRP-eQfeur", "Errors.Group.NotFound")
|
||||||
|
}
|
||||||
|
return group, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,29 +16,43 @@ type GroupWriteModel struct {
|
|||||||
Description string
|
Description string
|
||||||
|
|
||||||
State domain.GroupState
|
State domain.GroupState
|
||||||
|
|
||||||
|
UserIDs []string
|
||||||
|
existingUserIDs map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGroupWriteModel initializes a new instance of GroupWriteModel from the given Group.
|
// NewGroupWriteModel initializes a new instance of GroupWriteModel from the given Group.
|
||||||
func NewGroupWriteModel(id, orgID string) *GroupWriteModel {
|
func NewGroupWriteModel(groupID, orgID string, userIDs []string) *GroupWriteModel {
|
||||||
return &GroupWriteModel{
|
return &GroupWriteModel{
|
||||||
WriteModel: eventstore.WriteModel{
|
WriteModel: eventstore.WriteModel{
|
||||||
AggregateID: id,
|
AggregateID: groupID,
|
||||||
ResourceOwner: orgID,
|
ResourceOwner: orgID,
|
||||||
},
|
},
|
||||||
|
UserIDs: userIDs,
|
||||||
|
existingUserIDs: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GroupWriteModel) GetWriteModel() *eventstore.WriteModel {
|
||||||
|
return &g.WriteModel
|
||||||
|
}
|
||||||
|
|
||||||
// Query constructs a search query for retrieving group-related events based on the GroupWriteModel attributes.
|
// Query constructs a search query for retrieving group-related events based on the GroupWriteModel attributes.
|
||||||
func (g *GroupWriteModel) Query() *eventstore.SearchQueryBuilder {
|
func (g *GroupWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
eventTypes := []eventstore.EventType{
|
||||||
|
group.GroupAddedEventType,
|
||||||
|
group.GroupChangedEventType,
|
||||||
|
group.GroupRemovedEventType,
|
||||||
|
}
|
||||||
|
if g.UserIDs != nil {
|
||||||
|
eventTypes = append(eventTypes, group.GroupUsersAddedEventType, group.GroupUsersRemovedEventType)
|
||||||
|
}
|
||||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
ResourceOwner(g.ResourceOwner).
|
ResourceOwner(g.ResourceOwner).
|
||||||
AddQuery().
|
AddQuery().
|
||||||
AggregateTypes(group.AggregateType).
|
AggregateTypes(group.AggregateType).
|
||||||
AggregateIDs(g.AggregateID).
|
AggregateIDs(g.AggregateID).
|
||||||
EventTypes(
|
EventTypes(eventTypes...).Builder()
|
||||||
group.GroupAddedEventType,
|
|
||||||
group.GroupChangedEventType,
|
|
||||||
group.GroupRemovedEventType).Builder()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GroupWriteModel) Reduce() error {
|
func (g *GroupWriteModel) Reduce() error {
|
||||||
@@ -58,6 +72,14 @@ func (g *GroupWriteModel) Reduce() error {
|
|||||||
}
|
}
|
||||||
case *group.GroupRemovedEvent:
|
case *group.GroupRemovedEvent:
|
||||||
g.State = domain.GroupStateRemoved
|
g.State = domain.GroupStateRemoved
|
||||||
|
case *group.GroupUsersAddedEvent:
|
||||||
|
for _, userID := range e.UserIDs {
|
||||||
|
g.existingUserIDs[userID] = struct{}{}
|
||||||
|
}
|
||||||
|
case *group.GroupUsersRemovedEvent:
|
||||||
|
for _, userID := range e.UserIDs {
|
||||||
|
delete(g.existingUserIDs, userID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return g.WriteModel.Reduce()
|
return g.WriteModel.Reduce()
|
||||||
|
|||||||
131
internal/command/group_users.go
Normal file
131
internal/command/group_users.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
repo "github.com/zitadel/zitadel/internal/repository/group"
|
||||||
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Commands) AddUsersToGroup(ctx context.Context, groupID string, userIDs []string) (_ *domain.ObjectDetails, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
// precondition: check whether the group exists
|
||||||
|
group, err := c.checkGroupExists(ctx, groupID, userIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether the requester has permissions to add users to the group
|
||||||
|
err = c.checkPermissionAddUserToGroup(ctx, group.ResourceOwner, group.AggregateID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the users to the group
|
||||||
|
return c.addUsersToGroup(ctx, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) RemoveUsersFromGroup(ctx context.Context, groupID string, userIDs []string) (_ *domain.ObjectDetails, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
// precondition: check whether the group exists
|
||||||
|
group, err := c.checkGroupExists(ctx, groupID, userIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether the requester has permissions to remove users from the group
|
||||||
|
err = c.checkPermissionRemoveUserFromGroup(ctx, group.ResourceOwner, group.AggregateID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDsToRemove := group.getUserIDsToRemove()
|
||||||
|
if len(userIDsToRemove) == 0 {
|
||||||
|
// the userIDs are not present in the group; desired state achieved
|
||||||
|
return writeModelToObjectDetails(&group.WriteModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove users from the group
|
||||||
|
return c.pushAppendAndReduceDetails(ctx,
|
||||||
|
group,
|
||||||
|
repo.NewGroupUsersRemovedEvent(
|
||||||
|
ctx,
|
||||||
|
GroupAggregateFromWriteModel(ctx, &group.WriteModel),
|
||||||
|
userIDsToRemove,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) addUsersToGroup(ctx context.Context, group *GroupWriteModel) (*domain.ObjectDetails, error) {
|
||||||
|
userIDsToAdd := group.getUserIDsToAdd()
|
||||||
|
if len(userIDsToAdd) == 0 {
|
||||||
|
// no new users to add
|
||||||
|
return writeModelToObjectDetails(&group.WriteModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// precondition: check whether the users exist
|
||||||
|
for _, userID := range userIDsToAdd {
|
||||||
|
// check whether the user exists in the same organization as the group
|
||||||
|
_, err := c.checkUserExists(ctx, userID, group.ResourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add users to the group
|
||||||
|
return c.pushAppendAndReduceDetails(ctx,
|
||||||
|
group,
|
||||||
|
repo.NewGroupUsersAddedEvent(
|
||||||
|
ctx,
|
||||||
|
GroupAggregateFromWriteModel(ctx, &group.WriteModel),
|
||||||
|
userIDsToAdd,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserIDsToAdd returns the userIDs that are not already in the group
|
||||||
|
func (g *GroupWriteModel) getUserIDsToAdd() []string {
|
||||||
|
userIDsToAdd := make([]string, 0)
|
||||||
|
for _, userID := range g.UserIDs {
|
||||||
|
if _, ok := g.existingUserIDs[userID]; !ok && !slices.Contains(userIDsToAdd, userID) {
|
||||||
|
userIDsToAdd = append(userIDsToAdd, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userIDsToAdd
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserIDsToRemove returns the userIDs that are in the group and should be removed
|
||||||
|
// if a userID is not in the group, the desired state has already been achieved
|
||||||
|
func (g *GroupWriteModel) getUserIDsToRemove() []string {
|
||||||
|
userIDsToRemove := make([]string, 0)
|
||||||
|
for _, userID := range g.UserIDs {
|
||||||
|
if _, ok := g.existingUserIDs[userID]; ok && !slices.Contains(userIDsToRemove, userID) {
|
||||||
|
userIDsToRemove = append(userIDsToRemove, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userIDsToRemove
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeUserFromGroups returns the events to remove a user from multiple groups.
|
||||||
|
// This is needed when a user is deleted and subsequently needs to be removed from all groups.
|
||||||
|
// Note: Ensure that the groupIDs are retrieved via SearchGroupUsers before calling this method
|
||||||
|
func (c *Commands) removeUserFromGroups(ctx context.Context, userID string, groupIDs []string, resourceOwner string) ([]eventstore.Command, error) {
|
||||||
|
events := make([]eventstore.Command, 0, len(groupIDs))
|
||||||
|
for _, groupID := range groupIDs {
|
||||||
|
events = append(
|
||||||
|
events,
|
||||||
|
repo.NewGroupUsersRemovedEvent(
|
||||||
|
ctx,
|
||||||
|
&repo.NewAggregate(groupID, resourceOwner).Aggregate,
|
||||||
|
[]string{userID},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
541
internal/command/group_users_test.go
Normal file
541
internal/command/group_users_test.go
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/group"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommands_AddUsersToGroup(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
filterErr := errors.New("filter error")
|
||||||
|
pushErr := errors.New("push error")
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
groupID string
|
||||||
|
userIDs []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *domain.ObjectDetails
|
||||||
|
wantErr func(error) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "failed to get group write model, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilterError(filterErr),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
wantErr: func(err error) bool {
|
||||||
|
return errors.Is(err, filterErr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group not found, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
wantErr: zerrors.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing permission, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
wantErr: zerrors.IsPermissionDenied,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user not found, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(), // to get the user write model for user1
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
wantErr: zerrors.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "some users already exist in the group, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1"}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter( // to get the user write model to check if user1 exists
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewUserEvent("user2", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user2"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ID: "group1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all users already exist in the group, no events pushed, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ID: "group1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "failed to push events, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter( // to get the user write model for user1
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewUserEvent("user1", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPushFailed(
|
||||||
|
pushErr,
|
||||||
|
group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1"},
|
||||||
|
},
|
||||||
|
wantErr: func(err error) bool {
|
||||||
|
return errors.Is(err, pushErr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all users added, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter( // to get the user write model for user1
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewUserEvent("user1", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter( // to get the user write model for user2
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewUserEvent("user2", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1", "user2"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ID: "group1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all users added (with duplicate users), ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter( // to get the user write model for user1
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewUserEvent("user1", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter( // to get the user write model for user2
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewUserEvent("user2", "org1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1", "user2"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2", "user2", "user1"},
|
||||||
|
},
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ID: "group1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
checkPermission: tt.fields.checkPermission,
|
||||||
|
}
|
||||||
|
got, err := c.AddUsersToGroup(context.Background(), tt.args.groupID, tt.args.userIDs)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.True(t, tt.wantErr(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assertObjectDetails(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewGroupEvent(groupID, orgID string) *group.GroupAddedEvent {
|
||||||
|
return group.NewGroupAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate(groupID, orgID).Aggregate,
|
||||||
|
groupID,
|
||||||
|
"group description",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_RemoveUsersFromGroup(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
filterErr := errors.New("filter error")
|
||||||
|
pushErr := errors.New("push error")
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
groupID string
|
||||||
|
userIDs []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *domain.ObjectDetails
|
||||||
|
wantErr func(error) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "failed to get group write model, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilterError(filterErr),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
wantErr: func(err error) bool {
|
||||||
|
return errors.Is(err, filterErr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group not found, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
wantErr: zerrors.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing permission, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
wantErr: zerrors.IsPermissionDenied,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove users, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
group.NewGroupUsersRemovedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1", "user2"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2"},
|
||||||
|
},
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
ID: "group1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove users (with duplicates), ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
group.NewGroupUsersRemovedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1", "user2"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1", "user2", "user1", "user2", "user1", "user2"},
|
||||||
|
},
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
ID: "group1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "some users not in group, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
group.NewGroupUsersRemovedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user2"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user2", "user3"},
|
||||||
|
},
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
ID: "group1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all users not in group, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user3", "user4"},
|
||||||
|
},
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
ID: "group1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "failed to push events, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter( // to get the group write model
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupEvent("group1", "org1"),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPushFailed(
|
||||||
|
pushErr,
|
||||||
|
group.NewGroupUsersRemovedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
groupID: "group1",
|
||||||
|
userIDs: []string{"user1"},
|
||||||
|
},
|
||||||
|
wantErr: func(err error) bool {
|
||||||
|
return errors.Is(err, pushErr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
checkPermission: tt.fields.checkPermission,
|
||||||
|
}
|
||||||
|
got, err := c.RemoveUsersFromGroup(context.Background(), tt.args.groupID, tt.args.userIDs)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.True(t, tt.wantErr(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assertObjectDetails(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewGroupUsersAddedEvent(groupID, orgID string, userIds []string) *group.GroupUsersAddedEvent {
|
||||||
|
return group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate(groupID, orgID).Aggregate,
|
||||||
|
userIds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewUserEvent(userID, orgID string) *user.HumanAddedEvent {
|
||||||
|
return user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate(userID, orgID).Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
AllowedLanguage,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -172,3 +172,11 @@ func (c *Commands) checkPermissionUpdateGroup(ctx context.Context, resourceOwner
|
|||||||
func (c *Commands) checkPermissionDeleteGroup(ctx context.Context, resourceOwner, groupID string) error {
|
func (c *Commands) checkPermissionDeleteGroup(ctx context.Context, resourceOwner, groupID string) error {
|
||||||
return c.newPermissionCheck(ctx, domain.PermissionGroupDelete, group.AggregateType)(resourceOwner, groupID)
|
return c.newPermissionCheck(ctx, domain.PermissionGroupDelete, group.AggregateType)(resourceOwner, groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Commands) checkPermissionAddUserToGroup(ctx context.Context, resourceOwner, groupID string) error {
|
||||||
|
return c.newPermissionCheck(ctx, domain.PermissionGroupUserWrite, group.AggregateType)(resourceOwner, groupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) checkPermissionRemoveUserFromGroup(ctx context.Context, resourceOwner, groupID string) error {
|
||||||
|
return c.newPermissionCheck(ctx, domain.PermissionGroupUserDelete, group.AggregateType)(resourceOwner, groupID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func (c *Commands) UnlockUser(ctx context.Context, userID, resourceOwner string)
|
|||||||
return writeModelToObjectDetails(&existingUser.WriteModel), nil
|
return writeModelToObjectDetails(&existingUser.WriteModel), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) {
|
func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs, cascadingGroupIDs []string) (*domain.ObjectDetails, error) {
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing")
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing")
|
||||||
}
|
}
|
||||||
@@ -219,6 +219,15 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string,
|
|||||||
events = append(events, membershipEvents...)
|
events = append(events, membershipEvents...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove user from user groups
|
||||||
|
if len(cascadingGroupIDs) > 0 {
|
||||||
|
groupUserEvents, err := c.removeUserFromGroups(ctx, userID, cascadingGroupIDs, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
events = append(events, groupUserEvents...)
|
||||||
|
}
|
||||||
|
|
||||||
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/group"
|
||||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
"github.com/zitadel/zitadel/internal/repository/project"
|
"github.com/zitadel/zitadel/internal/repository/project"
|
||||||
@@ -1157,6 +1158,7 @@ func TestCommandSide_RemoveUser(t *testing.T) {
|
|||||||
userID string
|
userID string
|
||||||
cascadeUserMemberships []*CascadingMembership
|
cascadeUserMemberships []*CascadingMembership
|
||||||
cascadeUserGrants []string
|
cascadeUserGrants []string
|
||||||
|
cascadeUserGroups []string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
type res struct {
|
type res struct {
|
||||||
@@ -1506,13 +1508,87 @@ func TestCommandSide_RemoveUser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "remove user with user groups, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group2", "org1").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
instance.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilterOrganizationSettings("org1", false, false),
|
||||||
|
expectPush(
|
||||||
|
user.NewUserRemovedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
group.NewGroupUsersRemovedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
group.NewGroupUsersRemovedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group2", "org1").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
cascadeUserGroups: []string{"group1", "group2"},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
}
|
}
|
||||||
got, err := r.RemoveUser(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.cascadeUserMemberships, tt.args.cascadeUserGrants...)
|
got, err := r.RemoveUser(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.cascadeUserMemberships, tt.args.cascadeUserGrants, tt.args.cascadeUserGroups)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
} else if !tt.res.err(err) {
|
} else if !tt.res.err(err) {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func (c *Commands) userStateWriteModel(ctx context.Context, userID string) (writ
|
|||||||
return writeModel, nil
|
return writeModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) {
|
func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs, cascadingGroupIDs []string) (*domain.ObjectDetails, error) {
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing")
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing")
|
||||||
}
|
}
|
||||||
@@ -171,6 +171,15 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin
|
|||||||
events = append(events, membershipEvents...)
|
events = append(events, membershipEvents...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove user from user groups
|
||||||
|
if len(cascadingGroupIDs) > 0 {
|
||||||
|
groupUserEvents, err := c.removeUserFromGroups(ctx, userID, cascadingGroupIDs, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
events = append(events, groupUserEvents...)
|
||||||
|
}
|
||||||
|
|
||||||
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/group"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
"github.com/zitadel/zitadel/internal/repository/user"
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
@@ -1099,6 +1100,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
|
|||||||
userID string
|
userID string
|
||||||
cascadingMemberships []*CascadingMembership
|
cascadingMemberships []*CascadingMembership
|
||||||
grantIDs []string
|
grantIDs []string
|
||||||
|
groupIDs []string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
type res struct {
|
type res struct {
|
||||||
@@ -1351,6 +1353,81 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "remove user with user groups, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group1", "org1").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
group.NewGroupUsersAddedEvent(context.Background(),
|
||||||
|
&group.NewAggregate("group2", "org1").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
orgAgg,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilterOrganizationSettings("org1", false, false),
|
||||||
|
expectPush(
|
||||||
|
user.NewUserRemovedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"username",
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
group.NewGroupUsersRemovedEvent(ctx,
|
||||||
|
&group.NewAggregate("group1", "").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
group.NewGroupUsersRemovedEvent(ctx,
|
||||||
|
&group.NewAggregate("group2", "").Aggregate,
|
||||||
|
[]string{"user1"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
userID: "user1",
|
||||||
|
groupIDs: []string{
|
||||||
|
"group1",
|
||||||
|
"group2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "remove self, permission denied",
|
name: "remove self, permission denied",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
@@ -1389,7 +1466,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
|
|||||||
checkPermission: tt.fields.checkPermission,
|
checkPermission: tt.fields.checkPermission,
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := r.RemoveUserV2(ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...)
|
got, err := r.RemoveUserV2(ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs, tt.args.groupIDs)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ const (
|
|||||||
PermissionGroupWrite = "group.write"
|
PermissionGroupWrite = "group.write"
|
||||||
PermissionGroupRead = "group.read"
|
PermissionGroupRead = "group.read"
|
||||||
PermissionGroupDelete = "group.delete"
|
PermissionGroupDelete = "group.delete"
|
||||||
|
PermissionGroupUserWrite = "group.user.write"
|
||||||
|
PermissionGroupUserRead = "group.user.read"
|
||||||
|
PermissionGroupUserDelete = "group.user.delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants.
|
// ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants.
|
||||||
|
|||||||
@@ -1323,3 +1323,12 @@ func (i *Instance) DeleteGroup(ctx context.Context, t *testing.T, id string) *gr
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Instance) AddUsersToGroup(ctx context.Context, t *testing.T, groupID string, userIDs []string) *group_v2.AddUsersToGroupResponse {
|
||||||
|
resp, err := i.Client.GroupV2.AddUsersToGroup(ctx, &group_v2.AddUsersToGroupRequest{
|
||||||
|
Id: groupID,
|
||||||
|
UserIds: userIDs,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|||||||
220
internal/query/group_users.go
Normal file
220
internal/query/group_users.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/query/projection"
|
||||||
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
groupUsersTable = table{
|
||||||
|
name: projection.GroupUsersProjectionTable,
|
||||||
|
instanceIDCol: projection.GroupUsersColumnInstanceID,
|
||||||
|
}
|
||||||
|
GroupUsersColumnGroupID = Column{
|
||||||
|
name: projection.GroupUsersColumnGroupID,
|
||||||
|
table: groupUsersTable,
|
||||||
|
}
|
||||||
|
GroupUsersColumnUserID = Column{
|
||||||
|
name: projection.GroupUsersColumnUserID,
|
||||||
|
table: groupUsersTable,
|
||||||
|
}
|
||||||
|
GroupUsersColumnResourceOwner = Column{
|
||||||
|
name: projection.GroupUsersColumnResourceOwner,
|
||||||
|
table: groupUsersTable,
|
||||||
|
}
|
||||||
|
GroupUsersColumnCreationDate = Column{
|
||||||
|
name: projection.GroupUsersColumnCreationDate,
|
||||||
|
table: groupUsersTable,
|
||||||
|
}
|
||||||
|
GroupUsersColumnInstanceID = Column{
|
||||||
|
name: projection.GroupUsersColumnInstanceID,
|
||||||
|
table: groupUsersTable,
|
||||||
|
}
|
||||||
|
GroupUsersColumnSequence = Column{
|
||||||
|
name: projection.GroupUsersColumnSequence,
|
||||||
|
table: groupUsersTable,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupUsers struct {
|
||||||
|
SearchResponse
|
||||||
|
GroupUsers []*GroupUser
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUser struct {
|
||||||
|
GroupID string
|
||||||
|
ResourceOwner string
|
||||||
|
CreationDate time.Time
|
||||||
|
Sequence uint64
|
||||||
|
|
||||||
|
// user fields
|
||||||
|
UserID string
|
||||||
|
PreferredLoginName string
|
||||||
|
DisplayName string
|
||||||
|
AvatarUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUsersSearchQuery struct {
|
||||||
|
SearchRequest
|
||||||
|
Queries []SearchQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SearchGroupUsers(ctx context.Context, queries *GroupUsersSearchQuery, permissionCheck domain.PermissionCheck) (_ *GroupUsers, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
|
||||||
|
|
||||||
|
groupUsers, err := q.searchGroupUsers(ctx, queries, permissionCheckV2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if permissionCheck != nil && !permissionCheckV2 {
|
||||||
|
groupUsersCheckPermission(ctx, groupUsers, permissionCheck)
|
||||||
|
}
|
||||||
|
return groupUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGroupUsersUserIDsSearchQuery(userIDs []string) (SearchQuery, error) {
|
||||||
|
list := make([]interface{}, len(userIDs))
|
||||||
|
for i, value := range userIDs {
|
||||||
|
list[i] = value
|
||||||
|
}
|
||||||
|
return NewListQuery(GroupUsersColumnUserID, list, ListIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGroupUsersGroupIDsSearchQuery(groupIDs []string) (SearchQuery, error) {
|
||||||
|
list := make([]interface{}, len(groupIDs))
|
||||||
|
for i, value := range groupIDs {
|
||||||
|
list[i] = value
|
||||||
|
}
|
||||||
|
return NewListQuery(GroupUsersColumnGroupID, list, ListIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) searchGroupUsers(ctx context.Context, queries *GroupUsersSearchQuery, permissionCheckV2 bool) (groupUsers *GroupUsers, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
query, scan := prepareGroupUsersQuery()
|
||||||
|
query = groupUsersPermissionCheckV2(ctx, query, queries, permissionCheckV2)
|
||||||
|
eq := sq.And{
|
||||||
|
sq.Eq{
|
||||||
|
GroupUsersColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, zerrors.ThrowInvalidArgument(err, "QUERY-TTlfF6", "Errors.Query.InvalidRequest")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
|
||||||
|
groupUsers, err = scan(rows)
|
||||||
|
return err
|
||||||
|
}, stmt, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, zerrors.ThrowInternal(err, "QUERY-M5O50l", "Errors.Internal")
|
||||||
|
}
|
||||||
|
groupUsers.State, err = q.latestState(ctx, groupUsersTable)
|
||||||
|
return groupUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareGroupUsersQuery() (query sq.SelectBuilder, scan func(*sql.Rows) (*GroupUsers, error)) {
|
||||||
|
return sq.Select(
|
||||||
|
GroupUsersColumnGroupID.identifier(),
|
||||||
|
GroupUsersColumnUserID.identifier(),
|
||||||
|
HumanDisplayNameCol.identifier(),
|
||||||
|
LoginNameNameCol.identifier(),
|
||||||
|
GroupUsersColumnResourceOwner.identifier(),
|
||||||
|
HumanAvatarURLCol.identifier(),
|
||||||
|
GroupUsersColumnCreationDate.identifier(),
|
||||||
|
GroupUsersColumnSequence.identifier(),
|
||||||
|
countColumn.identifier(),
|
||||||
|
).From(groupUsersTable.identifier()).
|
||||||
|
LeftJoin(join(HumanUserIDCol, GroupUsersColumnUserID)).
|
||||||
|
LeftJoin(join(LoginNameUserIDCol, GroupUsersColumnUserID)).
|
||||||
|
Where(
|
||||||
|
sq.Eq{LoginNameIsPrimaryCol.identifier(): true},
|
||||||
|
).PlaceholderFormat(sq.Dollar),
|
||||||
|
func(rows *sql.Rows) (*GroupUsers, error) {
|
||||||
|
groupUsers := make([]*GroupUser, 0)
|
||||||
|
var count uint64
|
||||||
|
for rows.Next() {
|
||||||
|
g := new(GroupUser)
|
||||||
|
|
||||||
|
var (
|
||||||
|
displayName sql.NullString
|
||||||
|
avatarURL sql.NullString
|
||||||
|
preferredLoginName sql.NullString
|
||||||
|
)
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&g.GroupID,
|
||||||
|
&g.UserID,
|
||||||
|
&displayName,
|
||||||
|
&preferredLoginName,
|
||||||
|
&g.ResourceOwner,
|
||||||
|
&avatarURL,
|
||||||
|
&g.CreationDate,
|
||||||
|
&g.Sequence,
|
||||||
|
&count,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
g.DisplayName = displayName.String
|
||||||
|
g.AvatarUrl = avatarURL.String
|
||||||
|
g.PreferredLoginName = preferredLoginName.String
|
||||||
|
groupUsers = append(groupUsers, g)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, zerrors.ThrowInternal(err, "QUERY-JuX6i5", "Errors.Query.CloseRows")
|
||||||
|
}
|
||||||
|
return &GroupUsers{
|
||||||
|
GroupUsers: groupUsers,
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: count,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupUsersCheckPermission(ctx context.Context, groupUsers *GroupUsers, permissionCheck domain.PermissionCheck) {
|
||||||
|
groupUsers.GroupUsers = slices.DeleteFunc(groupUsers.GroupUsers,
|
||||||
|
func(gu *GroupUser) bool {
|
||||||
|
return permissionCheck(ctx, domain.PermissionGroupUserRead, gu.ResourceOwner, gu.GroupID) != nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupUsersPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, queries *GroupUsersSearchQuery, permissionCheckV2 bool) sq.SelectBuilder {
|
||||||
|
if !permissionCheckV2 {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
join, args := PermissionClause(
|
||||||
|
ctx,
|
||||||
|
GroupUsersColumnResourceOwner,
|
||||||
|
domain.PermissionGroupUserRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
return query.JoinClause(join, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *GroupUsersSearchQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
||||||
|
query = q.SearchRequest.toQuery(query)
|
||||||
|
for _, q := range q.Queries {
|
||||||
|
query = q.toQuery(query)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
266
internal/query/group_users_test.go
Normal file
266
internal/query/group_users_test.go
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
groupUsersStmt = regexp.QuoteMeta(
|
||||||
|
"SELECT projections.group_users1.group_id" +
|
||||||
|
", projections.group_users1.user_id" +
|
||||||
|
", projections.users14_humans.display_name" +
|
||||||
|
", projections.login_names3.login_name" +
|
||||||
|
", projections.group_users1.resource_owner" +
|
||||||
|
", projections.users14_humans.avatar_key" +
|
||||||
|
", projections.group_users1.creation_date" +
|
||||||
|
", projections.group_users1.sequence" +
|
||||||
|
", COUNT(*) OVER ()" +
|
||||||
|
" FROM projections.group_users1" +
|
||||||
|
" LEFT JOIN projections.users14_humans ON projections.group_users1.user_id = projections.users14_humans.user_id AND projections.group_users1.instance_id = projections.users14_humans.instance_id" +
|
||||||
|
" LEFT JOIN projections.login_names3 ON projections.group_users1.user_id = projections.login_names3.user_id AND projections.group_users1.instance_id = projections.login_names3.instance_id" +
|
||||||
|
" WHERE projections.login_names3.is_primary = $1")
|
||||||
|
|
||||||
|
groupUsersColumns = []string{
|
||||||
|
"group_id",
|
||||||
|
"user_id",
|
||||||
|
"display_name",
|
||||||
|
"login_name",
|
||||||
|
"resource_owner",
|
||||||
|
"avatar_key",
|
||||||
|
"creation_date",
|
||||||
|
"sequence",
|
||||||
|
"count",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GroupUsersPrepares(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
type want struct {
|
||||||
|
sqlExpectations sqlExpectation
|
||||||
|
err checkErr
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
prepare interface{}
|
||||||
|
want want
|
||||||
|
object interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "prepareGroupUsersQuery no result",
|
||||||
|
prepare: prepareGroupUsersQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
groupUsersStmt,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &GroupUsers{GroupUsers: []*GroupUser{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepareGroupUsersQuery with one result",
|
||||||
|
prepare: prepareGroupUsersQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
groupUsersStmt,
|
||||||
|
groupUsersColumns,
|
||||||
|
[][]driver.Value{
|
||||||
|
{
|
||||||
|
"group-id",
|
||||||
|
"user-id",
|
||||||
|
"display-name",
|
||||||
|
"login-name",
|
||||||
|
"resource-owner",
|
||||||
|
"avatar-key",
|
||||||
|
testNow,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &GroupUsers{
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: 1,
|
||||||
|
},
|
||||||
|
GroupUsers: []*GroupUser{
|
||||||
|
{
|
||||||
|
GroupID: "group-id",
|
||||||
|
UserID: "user-id",
|
||||||
|
ResourceOwner: "resource-owner",
|
||||||
|
CreationDate: testNow,
|
||||||
|
Sequence: 1,
|
||||||
|
PreferredLoginName: "login-name",
|
||||||
|
DisplayName: "display-name",
|
||||||
|
AvatarUrl: "avatar-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepareGroupUsersQuery with multiple results",
|
||||||
|
prepare: prepareGroupUsersQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
groupUsersStmt,
|
||||||
|
groupUsersColumns,
|
||||||
|
[][]driver.Value{
|
||||||
|
{
|
||||||
|
"group-id-1",
|
||||||
|
"user-id-1",
|
||||||
|
"display-name-1",
|
||||||
|
"login-name-1",
|
||||||
|
"resource-owner",
|
||||||
|
"avatar-key",
|
||||||
|
testNow,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group-id-1",
|
||||||
|
"user-id-2",
|
||||||
|
"display-name-2",
|
||||||
|
"login-name-2",
|
||||||
|
"resource-owner",
|
||||||
|
"avatar-key",
|
||||||
|
testNow,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &GroupUsers{
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: 2,
|
||||||
|
},
|
||||||
|
GroupUsers: []*GroupUser{
|
||||||
|
{
|
||||||
|
GroupID: "group-id-1",
|
||||||
|
UserID: "user-id-1",
|
||||||
|
ResourceOwner: "resource-owner",
|
||||||
|
CreationDate: testNow,
|
||||||
|
Sequence: 1,
|
||||||
|
PreferredLoginName: "login-name-1",
|
||||||
|
DisplayName: "display-name-1",
|
||||||
|
AvatarUrl: "avatar-key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupID: "group-id-1",
|
||||||
|
UserID: "user-id-2",
|
||||||
|
ResourceOwner: "resource-owner",
|
||||||
|
CreationDate: testNow,
|
||||||
|
Sequence: 1,
|
||||||
|
PreferredLoginName: "login-name-2",
|
||||||
|
DisplayName: "display-name-2",
|
||||||
|
AvatarUrl: "avatar-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepareGroupUsersQuery sql err",
|
||||||
|
prepare: prepareGroupUsersQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueryErr(
|
||||||
|
groupUsersStmt,
|
||||||
|
sql.ErrConnDone,
|
||||||
|
),
|
||||||
|
err: func(err error) (error, bool) {
|
||||||
|
if !errors.Is(err, sql.ErrConnDone) {
|
||||||
|
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||||
|
}
|
||||||
|
return nil, true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
object: (*GroupUsers)(nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GroupUsersCheckPermission(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want []*GroupUser
|
||||||
|
groupUsers *GroupUsers
|
||||||
|
permissions []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no permissions",
|
||||||
|
want: []*GroupUser{},
|
||||||
|
groupUsers: &GroupUsers{
|
||||||
|
GroupUsers: []*GroupUser{
|
||||||
|
{GroupID: "group1"}, {GroupID: "group2"}, {GroupID: "group3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
permissions: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "permissions for group1",
|
||||||
|
want: []*GroupUser{
|
||||||
|
{GroupID: "group1", UserID: "user1"},
|
||||||
|
},
|
||||||
|
groupUsers: &GroupUsers{
|
||||||
|
GroupUsers: []*GroupUser{
|
||||||
|
{GroupID: "group1", UserID: "user1"}, {GroupID: "group2", UserID: "user2"}, {GroupID: "group3", UserID: "user3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
permissions: []string{"group1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "permissions for group2",
|
||||||
|
want: []*GroupUser{
|
||||||
|
{GroupID: "group2", UserID: "user2"},
|
||||||
|
},
|
||||||
|
groupUsers: &GroupUsers{
|
||||||
|
GroupUsers: []*GroupUser{
|
||||||
|
{GroupID: "group1", UserID: "user1"}, {GroupID: "group2", UserID: "user2"}, {GroupID: "group3", UserID: "user3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
permissions: []string{"group2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "permissions for group1 and group2",
|
||||||
|
want: []*GroupUser{
|
||||||
|
{GroupID: "group1", UserID: "user1"},
|
||||||
|
{GroupID: "group2", UserID: "user1"},
|
||||||
|
},
|
||||||
|
groupUsers: &GroupUsers{
|
||||||
|
GroupUsers: []*GroupUser{
|
||||||
|
{GroupID: "group1", UserID: "user1"},
|
||||||
|
{GroupID: "group2", UserID: "user1"},
|
||||||
|
{GroupID: "group3", UserID: "user1"},
|
||||||
|
{GroupID: "group4", UserID: "user2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
permissions: []string{"group1", "group2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||||
|
for _, perm := range tt.permissions {
|
||||||
|
if resourceID == perm {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("not found")
|
||||||
|
}
|
||||||
|
groupUsersCheckPermission(context.Background(), tt.groupUsers, checkPermission)
|
||||||
|
require.Equal(t, tt.want, tt.groupUsers.GroupUsers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ func (g *groupProjection) Reducers() []handler.AggregateReducer {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Aggregate: group.AggregateType,
|
Aggregate: org.AggregateType,
|
||||||
EventReducers: []handler.EventReducer{
|
EventReducers: []handler.EventReducer{
|
||||||
{
|
{
|
||||||
Event: org.OrgRemovedEventType,
|
Event: org.OrgRemovedEventType,
|
||||||
@@ -145,6 +145,7 @@ func (g *groupProjection) reduceGroupChanged(event eventstore.Event) (*handler.S
|
|||||||
columns,
|
columns,
|
||||||
[]handler.Condition{
|
[]handler.Condition{
|
||||||
handler.NewCond(GroupColumnID, e.Aggregate().ID),
|
handler.NewCond(GroupColumnID, e.Aggregate().ID),
|
||||||
|
handler.NewCond(GroupColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||||
handler.NewCond(GroupColumnInstanceID, e.Aggregate().InstanceID),
|
handler.NewCond(GroupColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
},
|
},
|
||||||
), nil
|
), nil
|
||||||
@@ -159,6 +160,7 @@ func (g *groupProjection) reduceGroupRemoved(event eventstore.Event) (*handler.S
|
|||||||
e,
|
e,
|
||||||
[]handler.Condition{
|
[]handler.Condition{
|
||||||
handler.NewCond(GroupColumnID, e.Aggregate().ID),
|
handler.NewCond(GroupColumnID, e.Aggregate().ID),
|
||||||
|
handler.NewCond(GroupColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||||
handler.NewCond(GroupColumnInstanceID, e.Aggregate().InstanceID),
|
handler.NewCond(GroupColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
},
|
},
|
||||||
), nil
|
), nil
|
||||||
|
|||||||
213
internal/query/projection/group_test.go
Normal file
213
internal/query/projection/group_test.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package projection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/group"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GroupReduces(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
type args struct {
|
||||||
|
event func(t *testing.T) eventstore.Event
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||||
|
want wantReduce
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "reduceGroupAdded",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
group.GroupAddedEventType,
|
||||||
|
group.AggregateType,
|
||||||
|
[]byte(`{
|
||||||
|
"name": "group-name",
|
||||||
|
"description": "group-description"
|
||||||
|
}`),
|
||||||
|
),
|
||||||
|
eventstore.GenericEventMapper[group.GroupAddedEvent],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupProjection{}).reduceGroupAdded,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("group"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO projections.groups1 (id, name, resource_owner, instance_id, description, creation_date, change_date, sequence, state) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
"group-name",
|
||||||
|
"ro-id",
|
||||||
|
"instance-id",
|
||||||
|
"group-description",
|
||||||
|
anyArg{},
|
||||||
|
anyArg{},
|
||||||
|
uint64(15),
|
||||||
|
domain.GroupStateActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceGroupChanged name",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
group.GroupChangedEventType,
|
||||||
|
group.AggregateType,
|
||||||
|
[]byte(`{
|
||||||
|
"name": "updated-group-name"
|
||||||
|
}`),
|
||||||
|
),
|
||||||
|
eventstore.GenericEventMapper[group.GroupChangedEvent],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupProjection{}).reduceGroupChanged,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("group"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "UPDATE projections.groups1 SET (name, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (resource_owner = $5) AND (instance_id = $6)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"updated-group-name",
|
||||||
|
anyArg{},
|
||||||
|
uint64(15),
|
||||||
|
"agg-id",
|
||||||
|
"ro-id",
|
||||||
|
"instance-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceGroupChanged description",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
group.GroupChangedEventType,
|
||||||
|
group.AggregateType,
|
||||||
|
[]byte(`{
|
||||||
|
"description": "updated-group-description"
|
||||||
|
}`),
|
||||||
|
),
|
||||||
|
eventstore.GenericEventMapper[group.GroupChangedEvent],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupProjection{}).reduceGroupChanged,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("group"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "UPDATE projections.groups1 SET (description, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (resource_owner = $5) AND (instance_id = $6)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"updated-group-description",
|
||||||
|
anyArg{},
|
||||||
|
uint64(15),
|
||||||
|
"agg-id",
|
||||||
|
"ro-id",
|
||||||
|
"instance-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceGroupRemoved",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
group.GroupRemovedEventType,
|
||||||
|
group.AggregateType,
|
||||||
|
[]byte(`{
|
||||||
|
"description": "updated-group-description"
|
||||||
|
}`),
|
||||||
|
),
|
||||||
|
eventstore.GenericEventMapper[group.GroupRemovedEvent],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupProjection{}).reduceGroupRemoved,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("group"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.groups1 WHERE (id = $1) AND (resource_owner = $2) AND (instance_id = $3)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
"ro-id",
|
||||||
|
"instance-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceOwnerRemoved",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
org.OrgRemovedEventType,
|
||||||
|
org.AggregateType,
|
||||||
|
[]byte(`{
|
||||||
|
"description": "updated-group-description"
|
||||||
|
}`),
|
||||||
|
),
|
||||||
|
org.OrgRemovedEventMapper,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupProjection{}).reduceOwnerRemoved,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("org"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.groups1 WHERE (instance_id = $1) AND (resource_owner = $2)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"instance-id",
|
||||||
|
"agg-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
event := baseEvent(t)
|
||||||
|
got, err := tt.reduce(event)
|
||||||
|
if ok := zerrors.IsErrorInvalidArgument(err); !ok {
|
||||||
|
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
event = tt.args.event(t)
|
||||||
|
got, err = tt.reduce(event)
|
||||||
|
assertReduce(t, got, err, GroupProjectionTable, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
167
internal/query/projection/group_users.go
Normal file
167
internal/query/projection/group_users.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package projection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/group"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GroupUsersProjectionTable = "projections.group_users1"
|
||||||
|
|
||||||
|
GroupUsersColumnGroupID = "group_id"
|
||||||
|
GroupUsersColumnUserID = "user_id"
|
||||||
|
GroupUsersColumnResourceOwner = "resource_owner"
|
||||||
|
GroupUsersColumnInstanceID = "instance_id"
|
||||||
|
GroupUsersColumnSequence = "sequence"
|
||||||
|
GroupUsersColumnCreationDate = "creation_date"
|
||||||
|
)
|
||||||
|
|
||||||
|
type groupUsersProjection struct{}
|
||||||
|
|
||||||
|
func (g *groupUsersProjection) Name() string {
|
||||||
|
return GroupUsersProjectionTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGroupUsersProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||||
|
return handler.NewHandler(ctx, &config, new(groupUsersProjection))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*groupUsersProjection) Init() *old_handler.Check {
|
||||||
|
return handler.NewTableCheck(
|
||||||
|
handler.NewTable([]*handler.InitColumn{
|
||||||
|
handler.NewColumn(GroupUsersColumnGroupID, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(GroupUsersColumnUserID, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(GroupUsersColumnResourceOwner, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(GroupUsersColumnInstanceID, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(GroupUsersColumnSequence, handler.ColumnTypeInt64),
|
||||||
|
handler.NewColumn(GroupUsersColumnCreationDate, handler.ColumnTypeTimestamp),
|
||||||
|
},
|
||||||
|
handler.NewPrimaryKey(GroupUsersColumnInstanceID, GroupUsersColumnGroupID, GroupUsersColumnUserID),
|
||||||
|
handler.WithIndex(handler.NewIndex("user_id", []string{GroupUsersColumnUserID})),
|
||||||
|
handler.WithIndex(handler.NewIndex("group_id", []string{GroupUsersColumnGroupID})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *groupUsersProjection) Reducers() []handler.AggregateReducer {
|
||||||
|
return []handler.AggregateReducer{
|
||||||
|
{
|
||||||
|
Aggregate: group.AggregateType,
|
||||||
|
EventReducers: []handler.EventReducer{
|
||||||
|
{
|
||||||
|
Event: group.GroupUsersAddedEventType,
|
||||||
|
Reduce: g.reduceGroupUsersAdded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Event: group.GroupUsersRemovedEventType,
|
||||||
|
Reduce: g.reduceGroupUsersRemoved,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Aggregate: group.AggregateType,
|
||||||
|
EventReducers: []handler.EventReducer{
|
||||||
|
{
|
||||||
|
Event: group.GroupRemovedEventType,
|
||||||
|
Reduce: g.reduceGroupRemoved,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Aggregate: org.AggregateType,
|
||||||
|
EventReducers: []handler.EventReducer{
|
||||||
|
{
|
||||||
|
Event: org.OrgRemovedEventType,
|
||||||
|
Reduce: g.reduceOwnerRemoved,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Aggregate: instance.AggregateType,
|
||||||
|
EventReducers: []handler.EventReducer{
|
||||||
|
{
|
||||||
|
Event: instance.InstanceRemovedEventType,
|
||||||
|
Reduce: reduceInstanceRemovedHelper(GroupColumnInstanceID),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *groupUsersProjection) reduceGroupUsersAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
e, err := assertEvent[*group.GroupUsersAddedEvent](event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stmts := make([]func(eventstore.Event) handler.Exec, 0, len(e.UserIDs))
|
||||||
|
for _, userID := range e.UserIDs {
|
||||||
|
stmts = append(stmts, handler.AddCreateStatement(
|
||||||
|
[]handler.Column{
|
||||||
|
handler.NewCol(GroupUsersColumnGroupID, e.Aggregate().ID),
|
||||||
|
handler.NewCol(GroupUsersColumnUserID, userID),
|
||||||
|
handler.NewCol(GroupUsersColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||||
|
handler.NewCol(GroupUsersColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
|
handler.NewCol(GroupUsersColumnSequence, e.Sequence()),
|
||||||
|
handler.NewCol(GroupUsersColumnCreationDate, e.CreationDate()),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return handler.NewMultiStatement(e, stmts...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *groupUsersProjection) reduceGroupUsersRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
e, err := assertEvent[*group.GroupUsersRemovedEvent](event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stmts := make([]func(eventstore.Event) handler.Exec, 0, len(e.UserIDs))
|
||||||
|
for _, userID := range e.UserIDs {
|
||||||
|
stmts = append(stmts, handler.AddDeleteStatement(
|
||||||
|
[]handler.Condition{
|
||||||
|
handler.NewCond(GroupUsersColumnGroupID, e.Aggregate().ID),
|
||||||
|
handler.NewCond(GroupUsersColumnUserID, userID),
|
||||||
|
handler.NewCond(GroupUsersColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||||
|
handler.NewCond(GroupUsersColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return handler.NewMultiStatement(e, stmts...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *groupUsersProjection) reduceGroupRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
e, err := assertEvent[*group.GroupRemovedEvent](event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.NewDeleteStatement(
|
||||||
|
e,
|
||||||
|
[]handler.Condition{
|
||||||
|
handler.NewCond(GroupUsersColumnGroupID, e.Aggregate().ID),
|
||||||
|
handler.NewCond(GroupUsersColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||||
|
handler.NewCond(GroupUsersColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
|
},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *groupUsersProjection) reduceOwnerRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
e, err := assertEvent[*org.OrgRemovedEvent](event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.NewDeleteStatement(
|
||||||
|
e,
|
||||||
|
[]handler.Condition{
|
||||||
|
handler.NewCond(GroupUsersColumnResourceOwner, e.Aggregate().ID),
|
||||||
|
handler.NewCond(GroupUsersColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
|
},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
177
internal/query/projection/group_users_test.go
Normal file
177
internal/query/projection/group_users_test.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package projection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/group"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GroupUsersReduces(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
type args struct {
|
||||||
|
event func(t *testing.T) eventstore.Event
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||||
|
want wantReduce
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "reduceGroupUsersAdded",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
group.GroupUsersAddedEventType,
|
||||||
|
group.AggregateType,
|
||||||
|
[]byte(
|
||||||
|
`{
|
||||||
|
"userIds": ["user-id-1", "user-id-2", "user-id-3"]
|
||||||
|
}`),
|
||||||
|
), eventstore.GenericEventMapper[group.GroupUsersAddedEvent],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupUsersProjection{}).reduceGroupUsersAdded,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("group"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO projections.group_users1 (group_id, user_id, resource_owner, instance_id, sequence, creation_date) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
"user-id-1",
|
||||||
|
"ro-id",
|
||||||
|
"instance-id",
|
||||||
|
uint64(15),
|
||||||
|
anyArg{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO projections.group_users1 (group_id, user_id, resource_owner, instance_id, sequence, creation_date) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
"user-id-2",
|
||||||
|
"ro-id",
|
||||||
|
"instance-id",
|
||||||
|
uint64(15),
|
||||||
|
anyArg{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO projections.group_users1 (group_id, user_id, resource_owner, instance_id, sequence, creation_date) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
"user-id-3",
|
||||||
|
"ro-id",
|
||||||
|
"instance-id",
|
||||||
|
uint64(15),
|
||||||
|
anyArg{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceGroupUsersRemoved",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
group.GroupUsersRemovedEventType,
|
||||||
|
group.AggregateType,
|
||||||
|
[]byte(
|
||||||
|
`{
|
||||||
|
"userIds": ["user-id-1", "user-id-2"]
|
||||||
|
}`),
|
||||||
|
), eventstore.GenericEventMapper[group.GroupUsersRemovedEvent],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupUsersProjection{}).reduceGroupUsersRemoved,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("group"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.group_users1 WHERE (group_id = $1) AND (user_id = $2) AND (resource_owner = $3) AND (instance_id = $4)",
|
||||||
|
expectedArgs: []interface{}{"agg-id", "user-id-1", "ro-id", "instance-id"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.group_users1 WHERE (group_id = $1) AND (user_id = $2) AND (resource_owner = $3) AND (instance_id = $4)",
|
||||||
|
expectedArgs: []interface{}{"agg-id", "user-id-2", "ro-id", "instance-id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceGroupRemoved",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
group.GroupRemovedEventType,
|
||||||
|
group.AggregateType,
|
||||||
|
nil,
|
||||||
|
), eventstore.GenericEventMapper[group.GroupRemovedEvent],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupUsersProjection{}).reduceGroupRemoved,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("group"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.group_users1 WHERE (group_id = $1) AND (resource_owner = $2) AND (instance_id = $3)",
|
||||||
|
expectedArgs: []interface{}{"agg-id", "ro-id", "instance-id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceOwnerRemoved",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(
|
||||||
|
testEvent(
|
||||||
|
org.OrgRemovedEventType,
|
||||||
|
org.AggregateType,
|
||||||
|
nil,
|
||||||
|
), org.OrgRemovedEventMapper,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
reduce: (&groupUsersProjection{}).reduceOwnerRemoved,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("org"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.group_users1 WHERE (resource_owner = $1) AND (instance_id = $2)",
|
||||||
|
expectedArgs: []interface{}{"agg-id", "instance-id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := baseEvent(t)
|
||||||
|
got, err := tt.reduce(event)
|
||||||
|
if ok := zerrors.IsErrorInvalidArgument(err); !ok {
|
||||||
|
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
event = tt.args.event(t)
|
||||||
|
got, err = tt.reduce(event)
|
||||||
|
assertReduce(t, got, err, GroupUsersProjectionTable, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,7 +104,8 @@ var (
|
|||||||
MembershipFields *handler.FieldHandler
|
MembershipFields *handler.FieldHandler
|
||||||
PermissionFields *handler.FieldHandler
|
PermissionFields *handler.FieldHandler
|
||||||
|
|
||||||
GroupProjection *handler.Handler
|
GroupProjection *handler.Handler
|
||||||
|
GroupUsersProjection *handler.Handler
|
||||||
)
|
)
|
||||||
|
|
||||||
type projection interface {
|
type projection interface {
|
||||||
@@ -211,6 +212,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
|
|||||||
// Don't forget to add the new field handler to [ProjectInstanceFields]
|
// Don't forget to add the new field handler to [ProjectInstanceFields]
|
||||||
|
|
||||||
GroupProjection = newGroupProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["groups"]))
|
GroupProjection = newGroupProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["groups"]))
|
||||||
|
GroupUsersProjection = newGroupUsersProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["group_users"]))
|
||||||
|
|
||||||
InstanceRelationalProjection = newInstanceRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instances_relational"]))
|
InstanceRelationalProjection = newInstanceRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instances_relational"]))
|
||||||
OrganizationRelationalProjection = newOrgRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organizations_relational"]))
|
OrganizationRelationalProjection = newOrgRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organizations_relational"]))
|
||||||
@@ -403,6 +405,7 @@ func newProjectionsList() {
|
|||||||
HostedLoginTranslationProjection,
|
HostedLoginTranslationProjection,
|
||||||
OrganizationSettingsProjection,
|
OrganizationSettingsProjection,
|
||||||
GroupProjection,
|
GroupProjection,
|
||||||
|
GroupUsersProjection,
|
||||||
|
|
||||||
InstanceRelationalProjection,
|
InstanceRelationalProjection,
|
||||||
OrganizationRelationalProjection,
|
OrganizationRelationalProjection,
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ func init() {
|
|||||||
eventstore.RegisterFilterEventMapper(AggregateType, GroupAddedEventType, eventstore.GenericEventMapper[GroupAddedEvent])
|
eventstore.RegisterFilterEventMapper(AggregateType, GroupAddedEventType, eventstore.GenericEventMapper[GroupAddedEvent])
|
||||||
eventstore.RegisterFilterEventMapper(AggregateType, GroupChangedEventType, eventstore.GenericEventMapper[GroupChangedEvent])
|
eventstore.RegisterFilterEventMapper(AggregateType, GroupChangedEventType, eventstore.GenericEventMapper[GroupChangedEvent])
|
||||||
eventstore.RegisterFilterEventMapper(AggregateType, GroupRemovedEventType, eventstore.GenericEventMapper[GroupRemovedEvent])
|
eventstore.RegisterFilterEventMapper(AggregateType, GroupRemovedEventType, eventstore.GenericEventMapper[GroupRemovedEvent])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, GroupUsersAddedEventType, eventstore.GenericEventMapper[GroupUsersAddedEvent])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, GroupUsersRemovedEventType, eventstore.GenericEventMapper[GroupUsersRemovedEvent])
|
||||||
}
|
}
|
||||||
|
|||||||
74
internal/repository/group/user.go
Normal file
74
internal/repository/group/user.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GroupUsersAddedEventType = groupEventTypePrefix + "users.added"
|
||||||
|
GroupUsersRemovedEventType = groupEventTypePrefix + "users.removed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupUsersAddedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
UserIDs []string `json:"userIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGroupUsersAddedEvent(ctx context.Context, aggregate *eventstore.Aggregate, userIDs []string) *GroupUsersAddedEvent {
|
||||||
|
return &GroupUsersAddedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
GroupUsersAddedEventType,
|
||||||
|
),
|
||||||
|
UserIDs: userIDs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GroupUsersAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = *event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GroupUsersAddedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GroupUsersAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUsersRemovedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
UserIDs []string `json:"userIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGroupUsersRemovedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
userIDs []string,
|
||||||
|
) *GroupUsersRemovedEvent {
|
||||||
|
return &GroupUsersRemovedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
GroupUsersRemovedEventType,
|
||||||
|
),
|
||||||
|
UserIDs: userIDs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GroupUsersRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = *event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GroupUsersRemovedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GroupUsersRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import "protoc-gen-openapiv2/options/annotations.proto";
|
|||||||
import "validate/validate.proto";
|
import "validate/validate.proto";
|
||||||
|
|
||||||
import "zitadel/filter/v2/filter.proto";
|
import "zitadel/filter/v2/filter.proto";
|
||||||
|
import "zitadel/authorization/v2beta/authorization.proto";
|
||||||
|
|
||||||
package zitadel.group.v2;
|
package zitadel.group.v2;
|
||||||
|
|
||||||
@@ -54,6 +55,32 @@ message Group {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GroupUser {
|
||||||
|
// GroupID is the unique identifier of the user group.
|
||||||
|
string group_id = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629023906488334\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// OrganizationID is the unique identifier of the organization to which the group belongs.
|
||||||
|
string organization_id = 2 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629098906488334\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// User represents the user in the user group.
|
||||||
|
zitadel.authorization.v2beta.User user = 3;
|
||||||
|
|
||||||
|
// CreationDate is the timestamp when the user was added to the group.
|
||||||
|
google.protobuf.Timestamp creation_date = 4 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"2025-01-23T10:34:18.051Z\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
message GroupsSearchFilter {
|
message GroupsSearchFilter {
|
||||||
oneof filter {
|
oneof filter {
|
||||||
option (validate.required) = true;
|
option (validate.required) = true;
|
||||||
@@ -94,4 +121,22 @@ enum FieldName {
|
|||||||
FIELD_NAME_NAME = 2;
|
FIELD_NAME_NAME = 2;
|
||||||
FIELD_NAME_CREATION_DATE = 3;
|
FIELD_NAME_CREATION_DATE = 3;
|
||||||
FIELD_NAME_CHANGE_DATE = 4;
|
FIELD_NAME_CHANGE_DATE = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupUsersSearchFilter {
|
||||||
|
oneof filter {
|
||||||
|
option (validate.required) = true;
|
||||||
|
|
||||||
|
// Search for groups by user IDs
|
||||||
|
zitadel.filter.v2.InIDsFilter user_ids = 1;
|
||||||
|
// Search for users in groups by group IDs
|
||||||
|
zitadel.filter.v2.InIDsFilter group_ids = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GroupUserFieldName {
|
||||||
|
GROUP_USER_FIELD_NAME_UNSPECIFIED = 0;
|
||||||
|
GROUP_USER_FIELD_NAME_USER_ID = 1;
|
||||||
|
GROUP_USER_FIELD_NAME_GROUP_ID = 2;
|
||||||
|
GROUP_USER_FIELD_NAME_CREATION_DATE = 3;
|
||||||
}
|
}
|
||||||
@@ -298,6 +298,53 @@ service GroupService {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Users
|
||||||
|
//
|
||||||
|
// Adds one or more users to a group.
|
||||||
|
// The users should be in the same organization as the group.
|
||||||
|
// The request will not fail if some of the users could not be added to the group,
|
||||||
|
// and a list of failed user IDs is available in the response.
|
||||||
|
//
|
||||||
|
// Required permissions:
|
||||||
|
// - group.user.write
|
||||||
|
rpc AddUsersToGroup(AddUsersToGroupRequest) returns (AddUsersToGroupResponse) {
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove Users
|
||||||
|
//
|
||||||
|
// Removes one or more users from a group.
|
||||||
|
// If some of the users are not in the group, the request will still return a successful response as
|
||||||
|
// the desired state is already achieved.
|
||||||
|
//
|
||||||
|
// Required permissions:
|
||||||
|
// - group.user.delete
|
||||||
|
rpc RemoveUsersFromGroup(RemoveUsersFromGroupRequest) returns (RemoveUsersFromGroupResponse) {
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Group Users
|
||||||
|
//
|
||||||
|
// Allows searching for groups based on user IDs, or retrieving users based on group IDs
|
||||||
|
//
|
||||||
|
// Required permissions:
|
||||||
|
// - group.user.read
|
||||||
|
rpc ListGroupUsers(ListGroupUsersRequest) returns (ListGroupUsersResponse) {
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateGroupRequest {
|
message CreateGroupRequest {
|
||||||
@@ -460,4 +507,81 @@ message DeleteGroupResponse {
|
|||||||
example: "\"2025-08-11T15:00:00.051Z\"";
|
example: "\"2025-08-11T15:00:00.051Z\"";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AddUsersToGroupRequest {
|
||||||
|
// ID of the group to which the users should be added.
|
||||||
|
string id = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"69629012906488334\"";
|
||||||
|
},
|
||||||
|
(google.api.field_behavior) = REQUIRED
|
||||||
|
];
|
||||||
|
|
||||||
|
// UserIds is a list of IDs of the users who should be added to the group.
|
||||||
|
repeated string user_ids = 2 [
|
||||||
|
(validate.rules).repeated = {min_items: 1, items: {string: {min_len: 1, max_len: 200}}},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "[\"69629023906488334\",\"69622366012355662\"]";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddUsersToGroupResponse {
|
||||||
|
// ChangeDate is the timestamp when the users are added.
|
||||||
|
google.protobuf.Timestamp change_date = 2 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"2025-08-11T15:00:00.051Z\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveUsersFromGroupRequest {
|
||||||
|
// ID of the group from which the users should be removed.
|
||||||
|
string id = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629012906488334\"";
|
||||||
|
},
|
||||||
|
(google.api.field_behavior) = REQUIRED
|
||||||
|
];
|
||||||
|
|
||||||
|
// UserIds is a list of IDs of the users who should be removed the group.
|
||||||
|
repeated string user_ids = 2 [
|
||||||
|
(validate.rules).repeated = {min_items: 1, items: {string: {min_len: 1, max_len: 200}}},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "[\"69629023906488334\",\"69622366012355662\"]";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveUsersFromGroupResponse {
|
||||||
|
// ChangeDate is the timestamp when the users are removed.
|
||||||
|
google.protobuf.Timestamp change_date = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"2025-08-11T15:00:00.051Z\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListGroupUsersRequest {
|
||||||
|
// GroupUsersSearchFilter defines the criteria to list the user groups.
|
||||||
|
repeated GroupUsersSearchFilter filters = 1;
|
||||||
|
|
||||||
|
// Pagination and sorting.
|
||||||
|
zitadel.filter.v2.PaginationRequest pagination = 2;
|
||||||
|
|
||||||
|
// SortingColumn defines the field the result is sorted by.
|
||||||
|
optional GroupUserFieldName sorting_column = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListGroupUsersResponse {
|
||||||
|
// GroupUsers is the list of users present in the requested group, matching the search criteria
|
||||||
|
repeated GroupUser group_users = 1;
|
||||||
|
|
||||||
|
// Contains the total number of users matching the query and the applied limit.
|
||||||
|
zitadel.filter.v2.PaginationResponse pagination = 2;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user