User Groups

Group-based role and restriction assignment with tenant scoping, member management, and additive merge at login.

User Groups provide a scalable way to assign roles and field-level restrictions to large numbers of administrators. Instead of assigning roles individually to each admin, you create a group, add roles and restrictions to it, then add admins as members. All members automatically inherit the group's roles and restrictions on their next login.

Architecture

Each UserGroup belongs to a tenant and has junction links: AdminUserGroup for members, UserGroupRole for roles, and UserGroupRestriction for field-level security. Relations are configured with Cascade Delete on the UserGroup side, so deleting a group automatically removes member links, roles, and restrictions, but Restrict Delete on the Tenant side prevents removing a Tenant with active groups.

User Group Membership Mapping Tree
Tenant Entity
1:N (Restrict Delete)
UserGroup (Aggregate Root)
1:N (Cascade Delete)
AdminUserGroup Junction
UserGroupRole Junction
UserGroupRestriction
Admin User
Role Entity
Union Permissions
Merged Claims Set (JWT)

Domain Model & Configurations

The User Groups feature utilizes four domain entities: UserGroup (aggregate root), AdminUserGroup (many-to-many junction), UserGroupRole (many-to-many junction), and UserGroupRestriction (field restrictions). Database configurations enforce unique composite indexes on {TenantId, Code} for UserGroup, {AdminId, UserGroupId} for AdminUserGroup, and {UserGroupId, RoleId} for UserGroupRole to prevent duplicate mappings.

EntityPurposeDatabase TableKeys & Composite IndexesCascade Deletion Rule
UserGroupBelongs to Tenant (or null for global groups). Group names are localizable.UserGroupsPK: Id, Unique Composite: { TenantId, Code }, Indexes: TenantId, IsActive, IsDeletedTenant: Restrict (cannot delete Tenant if active groups exist).
AdminUserGroupLinks an Admin to a UserGroup (enrollment junction).AdminUserGroupsPK: Id, Unique Composite: { AdminId, UserGroupId }, Indexes: AdminId, UserGroupId, IsDeletedAdmin: Cascade, UserGroup: Cascade (junction deleted if either side is removed).
UserGroupRoleMaps group memberships to RBAC Roles.UserGroupRolesPK: Id, Unique Composite: { UserGroupId, RoleId }, Indexes: UserGroupId, RoleId, IsDeletedUserGroup: Cascade, Role: Restrict (cannot delete Role if mapped to active groups).
UserGroupRestrictionDefines group-level field restrictions per permission code.UserGroupRestrictionsPK: Id, Indexes: UserGroupId, IsDeletedUserGroup: Cascade (restrictions deleted when the group is deleted).
UserGroup.cs — Domain EntityC#
public class UserGroup : AuditableEntity<Guid>
{
    [Required]
    [MaxLength(100)]
    public string NameEn { get; set; } = string.Empty;

    [Required]
    [MaxLength(100)]
    public string NameAr { get; set; } = string.Empty;

    [Required]
    [MaxLength(50)]
    public string Code { get; set; } = string.Empty;

    [MaxLength(500)]
    public string? DescriptionEn { get; set; }

    [MaxLength(500)]
    public string? DescriptionAr { get; set; }

    public Guid? TenantId { get; set; }

    public new bool IsActive { get; set; } = true;

    // Navigation Properties
    public virtual Tenant? Tenant { get; set; }
    public virtual ICollection<AdminUserGroup> AdminUserGroups { get; set; } = [];
    public virtual ICollection<UserGroupRole> UserGroupRoles { get; set; } = [];
    public virtual ICollection<UserGroupRestriction> UserGroupRestrictions { get; set; } = [];
}

How It Works at Login

At authentication, the AdminRepository implements a single-query projection pattern (GetWithRolesAsync) to fetch direct and group-inherited roles and restrictions in one database round-trip. Group-inherited roles are projected into synthetic AdminRole instances (with Id = Guid.Empty) and appended to the admin's Roles collection. AdminSecurityService then performs an additive union merge of role-level and group-level field restrictions (where 'deny wins' for FLS).

Single-Query Projection & Additive Merge Flow
Admin Auth Trigger
AdminRepository Single-Query
Project Direct
Direct Roles
Group-Inherited Roles
Map In-Memory
Synthetic AdminRoles (Guid.Empty)
Additive Merge
Union-Merge FLS
Deny Wins Union
JWT Token & Claims

Member Management

Adding members is idempotent — posting an admin ID that is already a member silently succeeds. Removing a member removes the junction record; the admin retains all directly-assigned roles. The member list is queryable with admin metadata (name, email, status).

Role Assignment

Group roles use a nuke-and-pave pattern (PUT replaces all). This ensures the database always matches the UI state. Each role must belong to the same tenant as the group. Roles assigned via groups appear alongside directly-assigned roles in the admin's effective permission set.

Field Restrictions

Group restrictions follow the same model as role-level RestrictedFields. Each restriction targets a specific permission code and lists the fields to hide. At login, the system takes the UNION of all restricted fields across direct roles and all group memberships — if any source restricts 'salary', it's restricted regardless of other assignments.

UserGroupRestriction Configuration ExampleJSON
// Group-level field restriction stored in UserGroupRestrictions table
{
  "permissionCode": "admins.view",
  "restrictedFieldsJson": "[\"salary\", \"bankAccount\", \"ssn\"]"
}

// Additive Field-Level Security Merging Example:
// 1. Direct role restrictions:     ["salary"]
// 2. UserGroup A restrictions:    ["salary", "bankAccount", "ssn"]
// 3. UserGroup B restrictions:    ["nationalId"]
// 4. Merged result in JWT:        ["salary", "bankAccount", "ssn", "nationalId"] (Union - Deny Wins)

Cascade Operations

User Groups support bulk operations (activate, deactivate, delete, and their filter-based -all variants). If cascadeAdmins is enabled, deactivation or soft-delete cascades to group members. Cascade operations automatically skip Protected Admins (like the tenant creator). If cascadeAdmins is false, any orphaned admin who loses all role assignments is automatically transferred to the SYSTEM_DEFAULT fallback role.

Cascading Delete & Status Toggle Logic
UserGroup Operation (Delete/Deactivate)
Update/Soft-Delete UserGroup
Is cascadeAdmins == true?
Yes
Query Enrolled Members
Is Orphaned (No direct/other group roles)?
Is Admin Protected (Tenant Creator)?
Assign SYSTEM_DEFAULT Fallback Role
Retain Admin Account Active
Skip Admin (Immunity Safeguard)
Deactivate/Soft-Delete Admin

API Endpoints (18)

MethodEndpointDescriptionAuth
GET/api/v1/UserGroupsPaginated list (filterable by TenantId, search, status)🔒 Required
GET/api/v1/UserGroups/myTenantGroupsPaginated groups for current user's Tenant scope🔒 Required
GET/api/v1/UserGroups/{id}Detail response (contains active members, roles, and restrictions)🔒 Required
GET/api/v1/UserGroups/byTenantId/{tenantId}All groups belonging to a specific TenantId (SuperAdmin only)🔒 Required
POST/api/v1/UserGroupsCreate User Group for an explicit Tenant🔒 Required
POST/api/v1/UserGroups/createForMyTenantCreate User Group bound to current tenant scope🔒 Required
PUT/api/v1/UserGroups/{id}Update metadata and perform nuke-and-pave of RoleIds🔒 Required
DELETE/api/v1/UserGroups/{id}Soft-delete User Group (supports cascadeAdmins query param)🔒 Required
POST/api/v1/UserGroups/{groupId}/membersAdd members (admins) to a group (idempotent)🔒 Required
DELETE/api/v1/UserGroups/{groupId}/members/{adminId}Remove member admin from a group🔒 Required
PUT/api/v1/UserGroups/{groupId}/rolesNuke-and-pave sync of group roles🔒 Required
PUT/api/v1/UserGroups/{groupId}/restrictionsNuke-and-pave sync of field restrictions🔒 Required
POST/api/v1/UserGroups/bulk/activateBulk activate groups (supports cascadeAdmins body property)🔒 Required
POST/api/v1/UserGroups/bulk/deactivateBulk deactivate groups (supports cascadeAdmins body property)🔒 Required
POST/api/v1/UserGroups/bulk/deleteBulk soft-delete groups (supports cascadeAdmins body property)🔒 Required
POST/api/v1/UserGroups/bulk/activate-allBulk activate all matching filter (supports cascadeAdmins)🔒 Required
POST/api/v1/UserGroups/bulk/deactivate-allBulk deactivate all matching filter (supports cascadeAdmins)🔒 Required
POST/api/v1/UserGroups/bulk/delete-allBulk soft-delete all matching filter (supports cascadeAdmins)🔒 Required

Frontend Module

The frontend utilizes a clean MVVM pattern. UserGroupService communicates with the backend, UserGroupRepository validates contracts with Zod schema verification (UserGroupModelSchema), and UserGroupMapper maps response DTOs. The useUserGroupsViewModel coordinates CRUD states, including custom deleteDialog and statusDialog state handlers for cascading actions.

Component/FileMVVM Pattern RoleDescription / Responsibilities
UserGroupService.tsAPI Data ServiceCommunicates with backend PascalCase routes (/api/v1/UserGroups) and handles payload mapping.
UserGroupRepository.tsData Layer RepositoryMediates between UI and service. Leverages Zod schema validation to verify contract compliance.
UserGroupMapper.tsData MapperConverts response DTOs to UI domain entities, null-coalescing missing properties.
useUserGroupsViewModel.tsPresentation ViewModelConsumes repository via DI, drives table state, filters, and manages cascade delete/status modal dialogs.
UserGroupsView.tsxPresentation ViewRenders table columns, localized names, member badges, and links to bulk action execution.
UserGroupDetailView.tsxPresentation ViewTabbed workspace details (Members, Roles, Restrictions) supporting nuke-and-pave updates.