Files
zitadel/internal/command/group_users.go
Gayathri Vijayan ad8e8bf61f 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
2025-10-28 13:23:54 +00:00

132 lines
4.1 KiB
Go

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
}