Entity Groups: Grouped Permission UX¶
Problem¶
The permission model assigns access at the (role, scope) level — one role_permissions row per entity-scope pair. As entities grow (departments, grades, periods, curriculum, sections...), assigning permissions entity-by-entity becomes redundant. In practice, entities cluster into domain areas where access levels almost always match:
- If a user can write departments, they almost certainly can write grades and periods too
- If a user can read student anagraphic, they likely can read teacher and staff anagraphic too
The admin configuring roles shouldn't think entity-by-entity — they should think in domain groups.
Design: View-Layer Groups (No Schema Change)¶
Entity groups are a frontend/API convenience, not a database concept. The DB stays flat (individual role_permissions rows). Guards are unchanged. Groups exist as:
- Backend constants — a map of
group → entity[] - A grouped API response — wraps the flat permissions into group structure
- Frontend UI — group-based toggles that expand to individual entity-scope assignments
Why Not DB-Level Groups?¶
| Concern | View-layer groups | DB-level groups |
|---|---|---|
| Schema migration | None | New table + FK |
| Fine-grained overrides | Natural (flat rows) | Awkward (group vs entity conflict) |
| Guard complexity | Zero change | Must resolve group → entities |
| Future flexibility | Can upgrade to DB later | Harder to downgrade |
| Scale (~15 entities) | Sufficient | Over-engineered |
Entity Group Definitions¶
| Group ID | Label | Entities | Notes |
|---|---|---|---|
people |
People | students, teachers, staff | Core anagraphic CRUD |
academic-structure |
Academic Structure | departments, grades, periods, academic_years | Populated as these get permission entities |
platform |
Platform | users | User management + platform config |
teaching-schedule |
Teaching & Schedule | curriculum, timetable, sections | Future — not yet implemented |
Groups mirror the setup wizard groups (src/setup/constants/setup-groups.ts) but are permission-specific.
Backend Implementation (Future Epic)¶
Constants¶
src/common/constants/entity-groups.ts:
export enum EntityGroupId {
PEOPLE = 'people',
ACADEMIC_STRUCTURE = 'academic-structure',
PLATFORM = 'platform',
}
export interface EntityGroupDefinition {
id: EntityGroupId;
label: string;
description: string;
entities: EntityKeyValue[];
}
export const ENTITY_GROUPS: Record<EntityGroupId, EntityGroupDefinition> = { ... };
Plus a reverse lookup: getEntityGroup(entityKey): EntityGroupId | null.
Grouped Permissions Endpoint¶
GET /api/v1/permissions/grouped returns:
{
"groups": [
{
"id": "people",
"label": "People",
"entities": {
"students": {
"scopes": { "anagraphic": "WRITE", "sensitive": "READ" },
"actions": { "create": true, "delete": false }
},
"teachers": {
"scopes": { "anagraphic": "WRITE" },
"actions": { "create": true, "delete": true }
},
"staff": {
"scopes": { "anagraphic": "WRITE" },
"actions": { "create": true, "delete": true }
}
}
},
{
"id": "platform",
"label": "Platform",
"entities": {
"users": {
"scopes": { "profile": "READ" },
"actions": {}
}
}
}
],
"ungrouped": {}
}
The existing GET /api/v1/permissions stays unchanged (backward compatible).
Frontend Usage¶
Admin Role Editor¶
The role permission editor should present a two-level UI:
Level 1: Group Cards¶
┌─────────────────────────────────────┐
│ People [▼] │
│ ○ None ○ Read ● Write │
│ │
│ Students: WRITE ✓ │
│ Teachers: WRITE ✓ │
│ Staff: WRITE ✓ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Academic Structure [▼] │
│ ○ None ● Read ○ Write │
│ │
│ Departments: READ ✓ │
│ Grades: READ ✓ │
│ Periods: READ ✓ │
└─────────────────────────────────────┘
Behavior¶
-
Group-level toggle (None/Read/Write): sets ALL entities in the group to that access level. This is a convenience — it writes individual
role_permissionsrows per entity-scope. -
Expand (▼): shows per-entity overrides. A user can set the group to WRITE then downgrade one entity to READ. The group-level indicator then shows "Mixed" or the lowest common access.
-
Group access derivation: the group-level badge is computed from entity access:
- All entities same access → show that access
- Mixed → show "Mixed" with lowest access highlighted
-
No access on any → show "None"
-
Scope-level detail (optional expand within entity): for entities with multiple scopes (e.g., students has anagraphic + sensitive), show per-scope toggles. Most entities will have a single scope, so this level is rarely needed.
Actions Section¶
Below scopes, each entity's actions appear as checkboxes:
Actions are independent of groups — they're always per-entity. The group toggle only affects scope access.
Permission Display (Non-Admin View)¶
For non-admin users viewing their own permissions (GET /permissions/grouped), the frontend shows a read-only grouped view:
People
Students: Can view basic info and sensitive data
Teachers: Can view basic info
Staff: Can view basic info
Platform
Users: No access
API Calls for Role Management (Future)¶
When the admin saves a role with group-level access:
POST /api/v1/roles/:id/permissions
{
"groups": {
"people": "WRITE", // expands to all people entity scopes
"academic-structure": "READ" // expands to all academic structure entity scopes
},
"overrides": {
"students.sensitive": "NONE" // entity-scope override within a group
}
}
The backend:
1. Iterates ENTITY_GROUPS[groupId].entities
2. For each entity, fetches all permission_scopes
3. Creates/updates role_permissions rows with the group access level
4. Applies any per-entity-scope overrides on top
Migration Path¶
If entity groups ever need to be DB-level (e.g., tenant-customizable groups):
- Add
permission_entity_groupstable withid, key, label, sortOrder - Add
groupIdFK onpermission_entities - Seed from the constant definitions
- The constants become the seed source, guards still check per entity-scope
- The API can then serve tenant-specific group configurations
This is not needed now — constants suffice for system-defined groups.