Files
zitadel/internal/command/group.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

182 lines
5.1 KiB
Go

package command
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
repo "github.com/zitadel/zitadel/internal/repository/group"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type CreateGroup struct {
models.ObjectRoot
Name string
Description string
}
func (g *CreateGroup) IsValid() error {
if strings.TrimSpace(g.Name) == "" {
return zerrors.ThrowInvalidArgument(nil, "GROUP-m177lN", "Errors.Group.InvalidName")
}
return nil
}
// CreateGroup creates a new user group in an organization
func (c *Commands) CreateGroup(ctx context.Context, group *CreateGroup) (details *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
// create a unique group ID if not provided
if group.AggregateID == "" {
group.AggregateID, err = c.idGenerator.Next()
if err != nil {
return nil, err
}
}
if err = group.IsValid(); err != nil {
return nil, err
}
if group.ResourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "CMDGRP-msc0Tt", "Errors.Group.MissingOrganizationID")
}
if err = c.checkPermissionCreateGroup(ctx, group.ResourceOwner, group.AggregateID); err != nil {
return nil, err
}
// check whether the organization where the group should be created exists
err = c.checkOrgExists(ctx, group.ResourceOwner)
if err != nil {
return nil, zerrors.ThrowPreconditionFailed(nil, "CMDGRP-j1mH8l", "Errors.Org.NotFound")
}
// check if a group with the same ID already exists
groupWriteModel, err := c.getGroupWriteModelByID(ctx, group.AggregateID, group.ResourceOwner, nil)
if err != nil {
return nil, err
}
if groupWriteModel.State.Exists() {
return nil, zerrors.ThrowAlreadyExists(nil, "CMDGRP-shRut3", "Errors.Group.AlreadyExists")
}
err = c.pushAppendAndReduce(ctx,
groupWriteModel,
repo.NewGroupAddedEvent(ctx,
GroupAggregateFromWriteModel(ctx, &groupWriteModel.WriteModel),
group.Name,
group.Description,
))
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&groupWriteModel.WriteModel), nil
}
type UpdateGroup struct {
models.ObjectRoot
Name *string
Description *string
}
func (g *UpdateGroup) IsValid() error {
if g.Name != nil && strings.TrimSpace(*g.Name) == "" {
return zerrors.ThrowInvalidArgument(nil, "GROUP-dUNd3r", "Errors.Group.InvalidName")
}
return nil
}
// UpdateGroup updates a user group in an organization
func (c *Commands) UpdateGroup(ctx context.Context, groupUpdate *UpdateGroup) (details *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = groupUpdate.IsValid(); err != nil {
return nil, err
}
existingGroup, err := c.getGroupWriteModelByID(ctx, groupUpdate.AggregateID, groupUpdate.ResourceOwner, nil)
if err != nil {
return nil, err
}
if !existingGroup.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "CMDGRP-b33zly", "Errors.Group.NotFound")
}
if err = c.checkPermissionUpdateGroup(ctx, existingGroup.ResourceOwner, existingGroup.AggregateID); err != nil {
return nil, err
}
changedEvent := existingGroup.NewChangedEvent(
ctx,
GroupAggregateFromWriteModel(ctx, &existingGroup.WriteModel),
groupUpdate.Name,
groupUpdate.Description,
)
if changedEvent == nil {
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
}
err = c.pushAppendAndReduce(ctx,
existingGroup,
changedEvent)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
}
// DeleteGroup deletes a user group from an organization
func (c *Commands) DeleteGroup(ctx context.Context, groupID string) (details *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
existingGroup, err := c.getGroupWriteModelByID(ctx, groupID, "", nil)
if err != nil {
return nil, err
}
if !existingGroup.State.Exists() {
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
}
if err = c.checkPermissionDeleteGroup(ctx, existingGroup.ResourceOwner, existingGroup.AggregateID); err != nil {
return nil, err
}
err = c.pushAppendAndReduce(ctx,
existingGroup,
repo.NewGroupRemovedEvent(ctx,
GroupAggregateFromWriteModel(ctx, &existingGroup.WriteModel),
existingGroup.Name,
))
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
}
func (c *Commands) getGroupWriteModelByID(ctx context.Context, groupID, orgID string, userIDs []string) (*GroupWriteModel, error) {
groupWriteModel := NewGroupWriteModel(groupID, orgID, userIDs)
err := c.eventstore.FilterToQueryReducer(ctx, groupWriteModel)
if err != nil {
return nil, err
}
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
}