Skip to content

Role matrix v3 — management-preset realignment

1. Problem distillation

  • Product delivered a complete desired permission matrix (all_role_permissions_updated_v3.xlsx) for the five management presets: Curriculum Coordinator, Department Principal (department_head), HR Manager (hr), Director (principal), Front Office (secretary). Every sheet covers all 44 grantable scopes (everything except custom_fields.configuration, which stays granted-to-nobody).
  • Most rows already match the seed. The net change is: referent visibility opens up to hr/secretary/curriculum_coordinator (today referents are admin/principal/dept_head-only), audit-log READ opens to principal + department_head, evaluation scales stop being admin-only (READ for principal/secretary, WRITE for dept_head/coordinator), timetable templates open to dept_head (WRITE) and coordinator (READ), attendance becomes effective for HR (tenant-wide READ), curriculum_coordinator (owning-department READ), and department_head (department-scoped WRITE), the coordinator gains placement/selection write authority fenced by its curricula, and front office loses curriculum authoring (WRITE→READ + 4 actions revoked).
  • The sheets speak only in Read/Edit/No-access per scope. Actions are derived by rule (§ derivation rule below) plus the explicit Q&A decisions logged in §8.
  • Grants alone do not establish effective access. The referent directory and document masks, teacher document row policy, attendance aggregates and records, placement writes, and curriculum-selection read/write paths all need enforcement work alongside the seed change. Selection-window services already enforce their policy WHERE and need grants/tests only. Two genuinely new fences are required: the curriculum-shaped placement target fence and the query-backed department scope used by attendance aggregates and record writes.

Success criteria (observable behavior that proves this works): - GET /permissions for each of the five presets renders exactly the sheet matrix (with the two product-decided deviations in §8: no audit-log for HR, referents.identity READ added for HR), for both freshly provisioned and pre-v3 tenants. - An HR Manager / Front Office / Curriculum Coordinator session can list and open referents (coordinator only those linked to students with a dated selection in their curricula); Front Office can edit a referent's contact fields but gets 403 REFERENT_EMAIL_READ_ONLY on an email change. - HR can use every attendance READ surface tenant-wide, except communication operations that additionally require attendance.manage_communications; Curriculum Coordinator can use the same READ surfaces only for departments owning one of its parameter curricula. - A Department Principal can read the attendance day board / pending register / follow-up worklist restricted to their departments, record and correct cells in their departments (including past dates), and work the family-loop communications for their departments. - A Curriculum Coordinator can open/edit the selection window only for departments owning their parameter curricula; can PATCH a visible student's placement only onto such a department; can GET/PATCH a visible student's curriculum selection with the applicable-curricula response and target restricted to its parameter curricula. - Principal and HR can read curriculum selections tenant-wide; Front Office can read and write them tenant-wide. Department Principal and Curriculum Coordinator use their respective parameter fences. All school-side writers get management-editor semantics; referent lifecycle rules remain unchanged. - HR and Front Office see referent document metadata/files instead of the current non-staff null mask, and Curriculum Coordinator can read teacher documents without acquiring teacher profile write reach. - GET /roles and role-assignment responses display HR Manager, Front Office, and Department Principal consistently for global presets and tenant clones. - A Front Office session gets 403 on curriculum create/delete/grid-write and on the selection-window ops; existing tenants lose those grants via migration. - The data migration converges every affected global preset and existing tenant clone to the v3 delta while leaving unrelated grants, custom roles, and unaffected preset pairs untouched. - expected-preset-grants.ts drift guard and the Tier-1 coverage tests are green against the new matrices.

Non-goals (in-scope-shaped things this iteration is explicitly not doing): - No change to admin, teacher, referent, student presets (no sheets exist for them). - No new scopes, actions, entities, or /permissions shape changes — mapping only onto the existing catalogue. - No RecordAccessContext shape change. Write/management fences that must bind parameters to the role that granted access resolve that association from existing UserRole/UserRoleParameter and grant rows instead of trusting the flattened context arrays. - custom_fields.configuration stays granted to nobody. - No recipient-type gating on invitations (hr + secretary overlap stays as-is, per the 2026-07-02 deferral). - No dept-narrowing of the audit trail — the grant is all-or-nothing by design; product accepted tenant-wide reads for principal + dept_head. - Platform-owned evaluation-scale presets stay platform-admin-only (@PlatformAdminOnly routes untouched); tenant-side WRITE applies to tenant scales only. - Timetable-template writes for dept_head are school-wide (product-accepted); no dept fence on the template catalog or the assign action. - Bulk curriculum assignment (POST /students/curriculum-selections/bulk-assign) stays admin-only. - No new SelectionEditorRole database value. The existing ADMIN value remains the wire/storage compatibility value for every school-side management editor; REFERENT continues to mean a family editor.


2. Patterns survey

Analogous module/spec What we'd borrow What doesn't fit
prisma/seed/roles.ts (preset rule constants) + prisma/seed/helpers/expected-preset-grants.ts Exclusion-shaped matrices, *_WRITE_SCOPE_KEYS / *_EXCLUDED_SCOPE_KEYS / *_ACTION_KEYS constants mirrored by the drift guard OFFICE_SHARED_EXCLUDED_SCOPE_KEYS bundles the referents block + audit log for both hr and secretary; the new matrices diverge per role, so the shared const must be dissolved/refactored
prisma/migrations/20260702123500_curriculum_coordinator_hr_secretary_split/migration.sql + docs/15-seeding.md Data-only preset reconciliation across the global baseline and tenant clones Management clones are editable and normal reseeds do not propagate global changes to them; v3 therefore needs an explicit authoritative migration for every affected scope/action pair, not only the revocations
src/referents/referents.policy.ts + src/referents/referents.controller.ts (list allowlist) Role-branch policy + deliberately-narrower list allowlist + assertCallerIsSelfOrAdmin write gate New readers (hr, secretary, CURRICULUM-parametric) need branches; findAll today takes no ctx (all current list readers are tenant-wide) — parametric browse needs a ctx-narrowed list; secretary contacts WRITE needs the self-or-admin gate widened
src/students/students.policy.ts (CURRICULUM parametric branch, dated selections) The visibility shape for everything coordinator-linked: "students with a dated selection in my curricula", reused as a link-hop for referents Visibility only — the placement write fence (target department) doesn't exist for the CURRICULUM dim and must be invented (§7)
src/curriculum/selection-windows.policy.ts + src/curriculum/selection-window.service.ts The owning-departments derivation and the existing service-side policy.where(ctx) checks on prerequisites/open/edit No enforcement change is needed; the v3 delta is grants/actions, controller copy, and regression tests only
src/students/curriculum-selection.policies.ts + src/students/curriculum-selection.service.ts Hidden-404 record reach, dated DEPARTMENT narrowing, referent window/confirmation/carry locks, and response canWrite Principal/hr/secretary and CURRICULUM branches are missing; every non-admin management writer is currently stamped REFERENT, so the editor class and bypass semantics must be made explicit without a schema enum change
src/attendance/attendance.policy.ts + src/attendance/attendance-authority.ts + attendance-access.service.ts Grant-shaped gating, isSchoolWideRegister (excludes parametric roles), Y-set cell authority, board policy deliberately non-parametric HR and coordinator READ grants are dormant; dept_head "Edit" needs a third authority tier and every aggregate/service query needs an enforceable department intersection (§7)
src/permissions/permissions.service.ts + UserRoleParameter access-context loading Existing per-role permission compilation and assignment parameters RecordAccessContext.parameters is intentionally flattened; sensitive management/write fences need a reusable query that returns only parameter targets belonging to role assignments that independently satisfy the requested scope/action
src/referents/referents.service.ts + src/teachers/teachers.service.ts document configs Existing profile file masking and separate read/write row-policy intent Referent masking hard-codes the old staff roles; teacher document reads currently reuse TeachersWritePolicy, whose deliberate coordinator exclusion must remain for profile writes and therefore needs a separate document-read policy
docs/superpowers/specs/2026-08-11-communications-mailing-groups-rbac-split-design.md Precedent for coordinating a multi-preset grant sweep and drift-mirror update in one design Its reseed rollout does not fit editable management clones; the explicit v3 migration governs here
src/common/i18n/role-labels.catalog.ts + GET /role-assignments behavior in docs/04-rbac.md Localized catalogue labels and the stored Role.label returned by assignment surfaces Both sources must be aligned: catalogue-only relabeling would leave role-assignment responses stale

On-axis vs off-axis (ch16): the seed-matrix changes, relabels, authoritative preset migration, field-mask changes, and policy-branch additions are paved-path. Three items are off-axis inventions and are called out in §7: (1) the request-local, grant-qualified role slice used by sensitive payload/aggregate fences; (2) the curriculum-shaped placement target fence; (3) the query-backed attendance department scope plus department-scoped write authority (reverses a deliberate prior exclusion, with the enabling narrowing built this time). Curriculum-selection CURRICULUM reach itself reuses the dated StudentsPolicy predicate.


3. Architecture mapping

Primitive Apply? How Justify
Tenant scope yes All grant rows live on per-tenant role clones + the tenantId: null global baseline; the migration reconciles both and filters by preset key + isPreset Existing management clones are editable and are intentionally not updated by ordinary reseeding
Academic-year scope yes (attendance/selection only) Student/selection reach uses effectiveOn(schoolToday); attendance resolves curriculum-owned departments inside the active academic year and derives record/create authority from the dated attendance snapshot No forward-head fallback for historical access
RBAC entity key existing keys only none — no entity-keys.ts delta Matrix maps 1:1 onto the current catalogue
Scopes existing only referents.*, audit_log.configuration, evaluation_scales.configuration, timetable_templates.configuration, rooms.configuration, attendance.register, curricula.selection_window, students.assignment, students.curriculum_selection, etc. — no new seed entries Sheets were authored against our scope vocabulary
Actions existing only Grants added/removed per §6; no new action keys (read/update implicit as always) Derivation rule: "Edit" keeps existing create/delete-type actions; additions only where a §8 decision says so
Service base existing custom services Changes land in referents, teachers, attendance, student placement, curriculum selection, and PermissionsService; selection-window enforcement stays unchanged Grant + fence work, not CRUD
queries.ts shape existing files + one shared permission query/helper PermissionsService.resolveAuthorizedRoleSlice (name fixed here) takes the current ctx and returns qualifying active-view role keys plus assignment targets grouped by dimension for roles that independently satisfy a requested scope access and optional action. Referents compose policy WHERE; selection intersects curricula; attendance queries accept resolved allowedDepartmentIds and AND it into aggregates/records Prevents a READ-only parametric role's ids from borrowing another role's WRITE/action grant without changing RecordAccessContext
Error codes existing only Record/target reach failures are hidden 404s (REFERENT_NOT_FOUND where already used, otherwise NOT_FOUND with the existing entity/id params); foreign attendance cohorts use 403 ATTENDANCE_NOT_AUTHORIZED_FOR_COHORT; referent email remains 403 REFERENT_EMAIL_READ_ONLY; existing lifecycle/validation codes remain unchanged No new code or translation catalogue entry
DTO conventions description-only No fields or wire unions change. Update canWrite Swagger copy to describe management writers and referent lifecycle rules; existing scope-group masking exposes only granted fields Contract behavior changes without a shape change
File-backed sub-resources existing Referent document policy gains HR/secretary read reach and the profile mask treats them as school readers. Teacher documents get a dedicated read-row policy/config so coordinator READ does not weaken TeachersWritePolicy for profile/document mutations No new FileUsage; mixed-role write fences stay intact
Custom fields no Hidden platform-wide; custom_fields.configuration stays granted to nobody 2026-08-17 decision unchanged
Profile completeness no n/a — no field/requirement changes

4. Data model plan

Schema deltas

  • None. No new tables, columns, enums, or FKs. This is a grant/policy/service change only.
  • SelectionEditorRole remains { ADMIN, REFERENT }. For compatibility, ADMIN is interpreted as school-side management editor (platform/admin, secretary, DEPARTMENT-parametric writer, or CURRICULUM-parametric writer), not literally only the admin preset. The API enum and stored historical values do not change.

Migration shape

  • One data-only migration (no DDL): realign_management_preset_v3_grants_and_labels.
  • Select only preset roles (isPreset = true) with keys principal, hr, secretary, department_head, or curriculum_coordinator, including the tenantId: null global baseline and every tenant clone.
  • UPSERT every scope/access delta in §6 into RolePermission, including the secretary curricula.configuration downgrade to READ. Existing rows are updated and missing rows are inserted.
  • INSERT/UPSERT every newly granted action in §6 and DELETE the four revoked secretary actions: curricula.create, curricula.delete, curricula.open_selection_window, curricula.edit_selection_window.
  • UPDATE stored labels authoritatively by role key: hrHR Manager, secretaryFront Office, department_headDepartment Principal.
  • Leave all unrelated scope/action pairs, non-preset/custom roles, user assignments, and parameters untouched.
  • Upgrade contract: although management preset clones are normally editable, the v3 workbook is authoritative for the affected pairs. The migration intentionally overwrites historical tenant edits on only those listed pairs/actions/labels. This is a one-time product realignment, not a change to ordinary clone semantics.
  • prisma/seed/roles.ts remains the baseline for fresh databases and newly provisioned tenants. A production reseed is neither the rollout mechanism nor required for existing tenants; docs/15-seeding.md explicitly excludes editable management clones from propagation.
  • Precedent: 20260702123500_curriculum_coordinator_hr_secretary_split for data-only relabel/action cleanup, expanded here to deterministic scope/action UPSERTs because reseeding cannot reconcile existing management clones.
  • Hazards (ch12): data-only writes on grant/role rows; audit the role-key/scope-key joins, conflict targets, affected-row counts, and DELETE predicates before migrate dev. Fold-check against any uncommitted migration first. No DDL, hot-domain-table lock, or domain backfill.

Indexes and uniqueness

  • n/a — no schema change.

5. API surface

No new routes. Changed gating on existing routes:

Verb Path Decorators / policy Request DTO Response DTO Effective-access change
GET /referents @RequireScopes(REFERENTS, 'read'), explicit @RequireRoles, new @AppliesPolicyDimensions(ReferentsPolicy) ListReferentsQueryDto PaginatedResponseDto<ReferentResponseDto> Add hr/secretary/coordinator to the directory gate. Pass ctx into findAll, AND the policy WHERE into rows and count, and use ctx-aware masking. CURRICULUM callers see only referents linked to students with an effective selection in their parameter curricula; HR/secretary are tenant-wide
GET /referents/:id, /referents/:id/documents* read routes Existing scope gates + ReferentsPolicy / ReferentsDocumentsPolicy Path params only ReferentResponseDto, FileSlotDto[], or SignedFileUrlDto Add HR/secretary pass-through to both policies and CURRICULUM dated-selection narrowing to profile policy only. Coordinator has no document-policy branch. Treat HR/secretary as school document readers in profile masking; teacher/family non-self readers remain masked
PATCH /referents/:id @RequireScopes(REFERENTS, 'write'), @AppliesPolicy(ReferentsPolicy) UpdateReferentDto ReferentResponseDto Admit secretary as a school-side contact editor. Field mapping limits it to referents.contacts; email changes remain 403 REFERENT_EMAIL_READ_ONLY; non-visible ids remain 404 REFERENT_NOT_FOUND
GET /teachers/:id/documents* read routes Existing @RequireScope(TEACHERS, 'documents', 'read'); new TeachersDocumentsReadPolicy in read config Path params only FileSlotDto[] or SignedFileUrlDto Add role-keyed coordinator read reach. Keep TeachersWritePolicy for profile/document mutations, so the new pass-through cannot short-circuit DEPARTMENT write narrowing in a mixed-role session; no custom CURRICULUM inheritance
GET/POST/PATCH /selection-windows/prerequisites, /selection-windows, /selection-windows/:id Existing @RequireAction(...open/edit_selection_window) + @AppliesPolicy(SelectionWindowsPolicy) DepartmentScopedSelectionWindowQueryDto, OpenSelectionWindowDto, or UpdateSelectionWindowDto Existing selection-window/prerequisite DTOs Grants/actions open coordinator access. Enforcement is unchanged: prerequisites/open/edit already apply policy WHERE and its owning-departments CURRICULUM branch
PATCH /students/:id assignment fields Existing students.assignment WRITE field guard + StudentsPolicy Existing UpdateStudentDto Existing student response DTO Evaluate assignment record/target reach with the WRITE-qualified role slice. Add a CURRICULUM target limb: a tenant/year qualified curriculum must own the payload department. Preserve qualified admin/DEPARTMENT limbs as complete alternatives. Grade∈department remains. Unreachable target → department NOT_FOUND; a successful move may remove coordinator visibility
GET /students/:id/curriculum-selection @RequireScope(STUDENTS, 'curriculum_selection', 'read'), @AppliesPolicy(CurriculumSelectionReadPolicy) Path param only SelectionReadResponseDto Add principal/hr/secretary pass-through and dated CURRICULUM reach, evaluated with the READ-qualified context. Intersect applicableCurricula only when CURRICULUM is the sole qualified read limb; another complete qualified limb contributes its normal full view
PATCH /students/:id/curriculum-selection @RequireScope(STUDENTS, 'curriculum_selection', 'write'), @AppliesPolicy(CurriculumSelectionWritePolicy) WriteSelectionDto SelectionReadResponseDto Add secretary and dated CURRICULUM limbs. Resolve authority per record+target: complete school-side authority bypasses referent lifecycle and stamps ADMIN; otherwise complete referent authority retains family gates and stamps REFERENT. Neither → target NOT_FOUND; bulk assign remains admin-only
POST/PATCH/DELETE Tenant evaluation-scale routes Existing tenant scope/action gates Existing DTOs Existing evaluation-scale DTOs Grants alone open tenant scales to department_head/coordinator; platform preset routes remain @PlatformAdminOnly
POST/PATCH/DELETE Timetable/day/week-template and assignment routes Existing scope/action gates Existing DTOs Existing template DTOs Grants alone open the school-wide catalog to department_head and READ to coordinator
GET /audit-log Existing scope gate Existing list-query DTO Existing paginated audit DTO Grants alone open the full tenant trail to principal/department_head
GET/POST/PATCH Attendance routes enumerated in §7.2 Existing attendance scope/action gates + AttendancePolicy / AttendanceBoardPolicy / AttendanceCohortReadPolicy Existing attendance query/write DTOs Existing board/row/insight/write DTOs Add HR tenant-wide READ, curriculum-owner-department READ, and department_head department-scoped READ/WRITE. Every management limb consumes the resolved department fence; teacher limbs remain separate; communications still require attendance.manage_communications

Swagger considerations

  • JSDoc on the touched routes is FE-facing copy: update the role lists named in route JSDoc (/referents list currently says "Admin, principal, and department heads"), without leaking guard internals.
  • Update curriculum-selection canWrite description: it is true only when the caller has effective WRITE permission, record/target reach, and either school-side management semantics or all referent lifecycle gates. Do not describe SelectionEditorRole.ADMIN as admin-only.
  • Update attendance Swagger text that currently says past dates are administrator/school-wide-only: department-scoped writers may backfill records inside their allowed departments.
  • No response-shape changes; /permissions output changes are data, not shape.

6. RBAC seed plan

Seed file Delta
PermissionScope (rbac-catalogue.ts) none
PermissionAction (rbac-catalogue.ts) none
ScopeFieldMapping (rbac-catalogue.ts) none
Role grants (roles.ts) Full per-role delta below
*_SCOPES runtime constant none — no scope-field changes
role-labels.catalog.ts hr: en_US HR Manager, it_IT Responsabile HR; secretary: en_US Front Office, it_IT Segreteria; department_head remains en_US Department Principal; principal remains Director
expected-preset-grants.ts Mirror every delta below (drift guard)
Seed Role.label hrHR Manager; secretaryFront Office; department_headDepartment Principal (English stored-label convention; migration covers global and tenant preset rows)

Grant deltas (everything not listed is unchanged; sheets confirmed all other rows already match):

principal (Director) — remove from PRINCIPAL_EXCLUDED_SCOPE_KEYS: - evaluation_scales.configuration → READ - audit_log.configuration → READ

hr (HR Manager) — remove from exclusions → READ: - referents.identity, referents.contacts, referents.documents, referents.students - (referents.health stays excluded; audit_log.configuration stays excluded — product decision, deviates from the sheet, §8)

secretary (Front Office) - Widen: referents.contactsWRITE; referents.identity, referents.documents, referents.students → READ; evaluation_scales.configuration → READ - Narrow: curricula.configuration WRITE → READ (seed baseline + authoritative migration) - Action revocations (migration): curricula.create, curricula.delete, curricula.open_selection_window, curricula.edit_selection_window - (referents.health + audit_log.configuration stay excluded)

department_head (Department Principal) — remove from exclusions / upgrade: - rooms.configuration → READ - evaluation_scales.configuration → WRITE, + actions evaluation_scales.create, evaluation_scales.delete - timetable_templates.configuration → WRITE, + actions timetable_templates.create, timetable_templates.delete, timetable_templates.assign - attendance.register READ → WRITE, + actions attendance.take, attendance.manage_communications - audit_log.configuration → READ

curriculum_coordinator - Upgrades: students.assignment READ → WRITE; students.curriculum_selection READ → WRITE; curricula.selection_window READ → WRITE + actions curricula.open_selection_window, curricula.edit_selection_window - New READ (remove from exclusions): students.school_relationships, students.referents_and_guardians, teachers.employment, teachers.documents, referents.identity, referents.contacts, referents.students, rooms.configuration, timetable_templates.configuration - New WRITE: evaluation_scales.configuration + actions evaluation_scales.create, evaluation_scales.delete - Stays excluded: referents.documents, referents.health, all staff.*, students.health, students.documents, teachers.health, users.profile, invitations.management, audit_log.configuration, communications.management, custom_fields.configuration - No students.create/students.delete (explicit product decision)

Unchanged grants that require effective-access work: - hr: attendance.register READ already exists; policy/service changes make it tenant-wide on all non-communication read surfaces. - curriculum_coordinator: attendance.register READ already exists; it resolves to the departments owning parameter curricula. teachers.documents READ is new above and requires the separate document-read row policy. - principal / hr: students.curriculum_selection READ already exists; secretary: WRITE already exists; department_head: WRITE already exists. The curriculum-selection policies/service are aligned so these grants are no longer dormant or misclassified.

Constant restructure: OFFICE_SHARED_EXCLUDED_SCOPE_KEYS no longer fits (hr and secretary now diverge on referents; audit stays shared-excluded for both, but the referents block dissolves). Rework into smaller shared consts (e.g. admin-only-catalog block + per-role referent exclusions) so each role's set reads as its sheet.

Derivation rule (standing): "Edit" keeps whatever create/delete-type actions the role already holds on that entity and never silently adds new ones; additions/removals only via the §8 decisions above.

The migration applies this exact delta to existing global and tenant preset rows. The seed and expected-preset-grants.ts define the same end state for fresh tenants; neither is a substitute for the migration.


7. Effective-access work packages and divergence ledger

PermissionsService.resolveAuthorizedRoleSlice(ctx, requirement) is the shared anti-cross-lending helper for the work below. requirement names one entity/scope access (READ or WRITE) and optionally an action. It queries active user-role assignments restricted to ctx.roles (the active session view) and returns:

  • roleKeys: only roles whose own grants satisfy the complete requirement;
  • parameter ids grouped by dimension, preserving only parameters attached to those qualifying role assignments.

The slice can derive a copy of RecordAccessContext containing only those role keys/parameters for policy evaluation. Platform admin remains an explicit caller-side bypass. This helper does not alter compiled permissions, the original RecordAccessContext, or the repository's general union model; it is used only where a payload/aggregate fence needs to know which role assignment supplied the authority. Results are request-local/cached so attendance does not repeat the grant query per row.

7.1 Curriculum selection semantics

CurriculumSelectionReadPolicy becomes the complete reader policy for the workbook:

  • pass-through: platform/admin, principal, hr, secretary;
  • existing teacher and linked-referent branches remain;
  • existing DEPARTMENT branch remains dated by schoolToday;
  • new CURRICULUM branch copies the canonical dated predicate from StudentsPolicy: the student must have an effective selection in one of ctx.parameters.curriculumIds.

CurriculumSelectionWritePolicy remains stricter:

  • pass-through: platform/admin and secretary;
  • linked referent, dated DEPARTMENT, and dated CURRICULUM branches;
  • no principal, HR, or teacher write branch.

The GET/PATCH service pre-check evaluates these policies with the READ- or WRITE-qualified context derived from the shared slice, not the original flattened context. The controller decorators remain the coarse route gate. This prevents a parameter assignment with no selection grant from lending its ids to a different role's grant.

The service introduces a selection-specific resolveSelectionWriteAuthority result (SCHOOL_SIDE | REFERENT | NONE); generic isPrivileged is not broadened. It evaluates the current student and requested target, not merely the session's role names:

  • school-side authority uses the WRITE-qualified role slice and requires a matching platform/admin, secretary, dated DEPARTMENT, or dated CURRICULUM limb; the CURRICULUM limb also requires the target curriculum among the WRITE-qualified parameter ids;
  • referent authority independently requires the linked-student limb, StudentReferentLink.canWrite, OPEN window, no prior confirmation, and all carried-choice rules;
  • in a mixed session, school-side semantics win only when a school-side limb reaches this record+target. Otherwise a valid referent limb may authorize with referent semantics. A role from one limb cannot lend its row/target reach to another limb's lifecycle bypass.

School-side authority persists lastEditedBy = SelectionEditorRole.ADMIN; referent authority persists REFERENT. canWrite uses the same WRITE-qualified slice and requires at least one target reachable through either complete authority path; a custom parametric READ-only role must never receive canWrite: true, even when another role in the session grants WRITE. Swagger states that canWrite is aggregate and each submitted target is still authorization-checked.

For a caller whose only READ-qualified limb is CURRICULUM, the returned applicableCurricula is intersected with READ-qualified parameter ids. Any independently READ-qualified school-wide, DEPARTMENT, teacher, or linked-referent limb contributes its normal full read view. Student ids outside every complete record limb and target curricula outside every complete write-authority path are hidden 404s; ordinary applicability/shape failures inside an allowed path retain the existing selection validation codes (including SELECTION_CURRICULUM_NOT_APPLICABLE 422).

7.2 Attendance effective-access package

One query-backed resolveManagementAttendanceScope asks resolveAuthorizedRoleSlice for the route's required attendance access and optional action, then computes the management limb for the active academic year:

  • null means tenant-wide: platform or a qualified admin/principal/secretary/HR role;
  • a set for DEPARTMENT dimensions: only parameter ids attached to qualified role assignments;
  • a set for CURRICULUM dimensions: distinct Curriculum.departmentId values where curriculum id is among the qualified parameters and tenant/year match;
  • multiple narrowed branches union their sets; any valid school-wide branch short-circuits to null; an empty result fails closed.
  • teacher-only sessions return no management limb. The existing dated teacher cohort/Y-set checks remain an independent authority path on the endpoints that admit teachers.

This resolver is the load-bearing management narrowing for aggregate surfaces because AttendanceRecord has a denormalized departmentId but no relation by which a synchronous CURRICULUM policy branch can derive owning departments. AttendancePolicy, AttendanceBoardPolicy, and AttendanceCohortReadPolicy admit HR as pass-through and admit DEPARTMENT/CURRICULUM dimensions for routing metadata, but service/query code must resolve and apply allowedDepartmentIds whenever the management limb authorizes a request. Boot-time comments and tests must state that the pass-through CURRICULUM policy branch is admission-only, not row authorization.

Evaluate management and existing teacher authority as complete alternative limbs—never role reach from one plus a bypass from the other. Apply the management fence to every non-family, non-teacher-self attendance path it authorizes:

  • GET /attendance/groups, /groups/pending, /rows, /inconsistencies;
  • POST /attendance/records, PATCH /attendance/records/:id, GET /attendance/records/:id/history;
  • GET /attendance/follow-ups, GET /attendance/follow-ups/summary, PATCH /attendance/follow-ups/:id/ack;
  • GET /attendance/justifications and GET /attendance/justifications/:id/files/:fileId on the school-side insights controller. Family justification routes keep their existing self/linked-student fence and are not widened.

The exact mechanics are:

  • optional request departmentId filters are intersected, never substituted; an explicit foreign department/cohort is rejected with 403 ATTENDANCE_NOT_AUTHORIZED_FOR_COHORT;
  • list/board/follow-up/justification queries AND departmentId IN allowedDepartmentIds; no post-query filtering or count-before-filtering;
  • existing record/history/update ids outside the resolved fence use the current hidden NOT_FOUND response;
  • create authority derives the department from the expected-attendance/placement snapshot on the requested date; update authority uses the record's frozen departmentId;
  • Department Principal may take/correct attendance, including past dates, only inside the resolved DEPARTMENT set. It may acknowledge follow-ups and manage justifications there because it also receives attendance.manage_communications;
  • HR is tenant-wide READ only. Coordinator is owning-department READ only. Neither receives attendance.take nor attendance.manage_communications, so write and family-loop communication routes remain 403 at the existing action guard; a coordinator's curriculum ids cannot expand a department head's WRITE/action slice in a mixed session;
  • /rows, /inconsistencies, justification-file reads, and record operations retain their existing teacher cohort/taught-student/Y-set limb; board/pending/follow-up/office-justification lists still have no teacher limb. Teacher-day, family surfaces, and school-wide admin/secretary/principal behavior remain unchanged.

7.3 File masking and document-row policies

  • Referent response mapping defines schoolDocumentReader as platform/admin, principal, department_head, HR, or secretary. Those roles receive document slots/metadata when their grants permit; teacher/family non-self rows remain null-masked. findAll now receives ctx and uses the identical mapper path as findById.
  • ReferentsDocumentsPolicy adds HR/secretary pass-through for document list/download row reach. Coordinator/CURRICULUM is intentionally absent because the matrix withholds referents.documents. The existing self-or-admin document mutation assertion remains, so adding a read role cannot widen writes in a mixed-role session.
  • Teacher documents use separate read and write configs: TeachersDocumentsReadPolicy adds role-keyed coordinator pass-through; all mutations and teacher profile PATCH/DELETE continue to use unchanged TeachersWritePolicy. This preserves its mixed-role narrowing invariant.

Divergence ledger

Pattern We diverge by Reason Tradeoff accepted
Management preset clones are editable and normal reseeds do not reconcile them A one-time authoritative migration overwrites only the v3-listed pairs/actions/labels on every existing preset clone The workbook is a product-wide realignment and must also narrow old tenants Historical tenant customization on affected pairs is intentionally lost; all unrelated customization remains
RecordAccessContext.parameters flattens all active assignments Add a request-local qualified role slice for selection, placement, and attendance management fences These paths must bind a scope/action grant to the parameters of the role assignment that supplied it One extra cached permission/assignment query on affected requests; no global context or response-shape change
AttendanceBoardPolicy deliberately non-parametric (2026-08-04) Add DEPARTMENT/CURRICULUM admission together with a mandatory query-backed department intersection Workbook grants must be effective without exposing tenant-wide aggregates Every aggregate path carries one more resolved fence; omissions are guarded by unit/E2E coverage
attendance-authority.ts two-tier actor (isSchoolWide / teacher Y-set) Add department-scoped writer, including past dates inside its set Department Principal is compliance-side, not a teacher, while isSchoolWideRegister correctly refuses parametric roles Past-write rule becomes school-wide or department-scoped within the allowed set
No CURRICULUM target fence on placement Add it as an independent target-authority limb: target department owns at least one parameter curriculum Coordinator receives assignment WRITE A successful move may remove the student from subsequent coordinator visibility; mixed roles use the union of complete limbs, not cross-limb reach
Referent list was tenant-wide and context-free Compose policy WHERE and ctx-aware masking into findAll Coordinator list must be narrowed and HR/secretary document fields must not be falsely masked Parametric list query adds a dated selection join
SelectionEditorRole.ADMIN name Retain the value for all school-side management editors Adding/renaming an enum would create a migration and wire break for no behavioral gain The enum name is legacy; Swagger/service docs define its broader meaning
One teacher document config reused TeachersWritePolicy for reads and writes Split read-row and write-row configs Coordinator needs tenant-wide document READ but must not become a write-policy pass-through Small config duplication preserves mixed-role safety
assertCallerIsSelfOrAdmin on referent profile writes Admit secretary on profile PATCH only Front Office receives referent contacts WRITE Field scopes and REFERENT_EMAIL_READ_ONLY remain the final fences
Selection-window actions were withheld from CURRICULUM roles Grant them against the already-enforced owning-departments policy Product decision (§8) and implementation already support the fence No service logic change; custom CURRICULUM roles with the actions inherit the same derived reach

8. Pushback log

US says (workbook) Conflicts with Proposed instead Status
HR: audit log Read Audit READ = full tenant-wide trail incl. surfaces HR can't otherwise read Audit READ only for Director + Department Principal; HR stays excluded Resolved (user, 2026-08-20)
HR: referents contacts/documents/linked-students Read but personal details No access Nameless referent rows — identity is the anchor scope everywhere else Add referents.identity READ for HR Resolved (user: "include identity")
Coordinator: selection window Edit 2026-07-02 design: windows are per-department; id-scoped curriculum role can't validate Option (a): grant window actions, fence = owning departments of parameter curricula Resolved (user picked a)
Coordinator: students Academic details Edit No curriculum-shaped placement write fence exists Build the fence (target dept owns ≥1 parameter curriculum); no student create/delete Resolved (user confirmed)
Dept Principal: evaluation scales / timetable templates Edit Both are admin-only shared tenant catalogs; parametric role edits are school-wide Accept tenant-wide writes; grant the existing actions (create/delete, templates also assign) Resolved (user: tenant-/school-wide OK)
Front Office: curricula Configuration Read Secretary currently holds grid WRITE + create/delete + window actions (2026-07-02 grant) Revoke: WRITE→READ + DELETE migration for the 4 actions Resolved (user confirmed)
Dept Principal: attendance Edit (scope column can't express actions) Attendance write authority is grant+policy+service-tiered, not scope-only Grant attendance.take + attendance.manage_communications; build the dept-scoped authority + dept-intersected boards Resolved (user confirmed)
HR / Coordinator: attendance Read Their READ grants already exist, but current attendance policies omit both roles and aggregate routes cannot enforce curriculum-derived narrowing HR tenant-wide READ; coordinator READ only in departments owning parameter curricula, through the shared query-backed resolver; neither receives communication/write actions Resolved by workbook fidelity (no documented deviation)
Coordinator referent rows: Read Referents have no curriculum relation (teacher reads were solved by pass-through) Narrow to referents linked to students with a dated selection in parameter curricula (link-hop over the StudentsPolicy shape) Resolved (user picked recommended)
Principal/HR/Front Office/Dept Principal curriculum-selection rows Existing policies admit only admin/teacher/referent/DEPARTMENT, and the service treats every non-admin writer as a referent Add the exact read/write branches and classify school-side management writers separately; restrict coordinator targets/response to parameter curricula Resolved by workbook fidelity (no documented deviation)
Coordinator: teacher documents Read Document reads currently reuse a write policy that intentionally excludes coordinator to prevent mixed-role write widening Separate teacher document read-row and write-row policies/configs Resolved by least-privilege design
Sheet titles "HR Manager" / "Front Office" / "Department Principal" Catalogue and stored labels disagree (HR, Front office, Department Head in seed) and assignment responses use stored text Align catalogue, seed, and migrated global/tenant preset labels; keys unchanged Resolved (user: rename labels)

9. Deferrals

  • Invitations recipient-type gate (hr vs secretary overlap) — unchanged since 2026-07-02 — follow-up: revisit when product splits invitation audiences.
  • Dept-narrowed audit trail — product accepted tenant-wide; if a narrower trail is ever wanted it's a new read surface, not a grant tweak — follow-up: none planned.
  • Coordinator on bulk-assign — stays admin-only; single-student selection writes cover the coordinator workflow — follow-up: revisit on product ask.
  • Teacher/staff sheets, family-role sheets — workbook has none; presets untouched — follow-up: next workbook version.
  • Backoffice preset editor UI — no UI work. New tenants clone the updated global baseline; existing tenant clones are reconciled once by the authoritative v3 migration and then remain editable under the normal seeding contract — follow-up: none.
  • Sequencing option, not a scope cut: the attendance package (§7.2) is the largest work item and is independently reviewable — the plan may order it after the grant/document/selection work, but the v3 outcome is incomplete until it lands.

10. Open questions

All resolved in chat 2026-08-20 (see §8). None outstanding.


11. Verification plan

  • Unit specs:
  • PermissionsService.resolveAuthorizedRoleSlice: inactive assignments and roles outside ctx.roles/the active view excluded; role keys and parameters retained only when that same role satisfies access/action; READ does not satisfy WRITE; missing optional action excludes the role; multi-assignment/dimension union and platform bypass contract covered; original ctx remains immutable.
  • Referents policy/service: HR/secretary pass-through; CURRICULUM dated link-hop; list WHERE + pagination count use the same narrowing; documents policy excludes coordinator; HR/secretary are unmasked; teacher/family non-self remains masked; secretary contacts write and email refusal.
  • Teachers policy/document service: coordinator can list/download documents through TeachersDocumentsReadPolicy; mutation still uses unchanged TeachersWritePolicy; a coordinator+department_head mixed session cannot use coordinator pass-through to widen a teacher profile/document write.
  • Attendance department resolver: HR → tenant-wide, qualified DEPARTMENT ids, qualified CURRICULUM ids → tenant/year-owned department ids, union, school-wide short-circuit, empty/foreign ids fail closed; coordinator READ ids cannot expand department-head WRITE/action authority in a mixed session.
  • attendance-authority.spec.ts: department-scoped writer can write in-department cells including past dates; foreign cell refused; create uses dated expected snapshot; update uses frozen record department; teacher Y-set and school-wide tiers unchanged.
  • Attendance board/insights/service specs: each management limb in §7.2 ANDs the department fence into data and counts; explicit foreign department/cohort → ATTENDANCE_NOT_AUTHORIZED_FOR_COHORT; foreign record id → hidden NOT_FOUND; HR/coordinator cannot take attendance or manage communications; teacher-only and mixed-role requests keep complete, independent teacher/management authority paths.
  • Selection-window specs: coordinator actions succeed only for policy-visible owning departments using the existing service WHERE checks; secretary is rejected after action revocation. No replacement enforcement path is introduced.
  • Students placement: coordinator target owning department succeeds; foreign target → department NOT_FOUND; row reach stays dated; a READ-only param role cannot lend ids to another assignment-scope writer; department/admin paths unchanged.
  • Curriculum-selection policies: principal/hr/secretary pass-through as specified; CURRICULUM student reach is dated; read-only roles have canWrite: false; CURRICULUM-only responses/targets are intersected; mixed school-wide branch follows union semantics.
  • Curriculum-selection lifecycle: complete secretary/DEPARTMENT/CURRICULUM authority paths bypass referent locks, stamp ADMIN, and still require effective WRITE; referent behavior/stamp is unchanged; mixed sessions cannot combine referent row reach with a management bypass; foreign target → curriculum NOT_FOUND; allowed-but-inapplicable target keeps 422 SELECTION_CURRICULUM_NOT_APPLICABLE.
  • Seed/drift/labels: v3 role constants equal expected-preset-grants; role-label catalogue pins all management keys/locales; seed stored labels match English catalogue values.
  • E2E specs:
  • rbac-presets.e2e-spec.ts: full expected-grant matrices for all five presets rewritten to the v3 sheets (+ the two §8 deviations).
  • Upgrade smoke from a pre-v3 database: provision at least one tenant clone with v2 grants/labels, apply realign_management_preset_v3_grants_and_labels, then assert every listed global/tenant delta (including additions, secretary downgrade/action deletes, and all three labels) plus preservation of an unrelated customized grant and a custom role. A fresh-seed-only test does not satisfy this requirement.
  • hr-secretary-split.e2e-spec.ts: HR referent/document reads and tenant-wide attendance reads; secretary referent contact edit + email fence, curriculum authoring/window 403s, and school-side curriculum-selection write semantics.
  • curriculum-coordinator.e2e-spec.ts: referent list narrowing, teacher document read, placement fence, selection response/target fence, window open/edit through existing policy, owning-department attendance reads, evaluation-scale write, template READ.
  • Principal/department-head coverage: principal curriculum-selection read + audit READ; department head selection management semantics, audit READ, and catalog writes accepted as documented.
  • New dept-head-attendance.e2e-spec.ts (or extension): groups/pending/rows/inconsistencies/follow-ups/summary/ack/justification-file department intersection, record create/edit/history in/out of department, past-write admission, and communication action fence.
  • /permissions, /roles, and role-assignment E2E: per-preset scopes/actions match the sheets and the three stored/display labels are aligned.
  • Drift guards: expected-preset-grants + Tier-1 coverage tests updated in the same change.
  • E2E fixtures follow feedback_e2e_isolation_patterns (per-worker clones; parametric fixtures anchor as Teacher-with-zero-TeacherDepartment per the Carla/Diana pattern).
  • Migration audit: before applying, inspect the generated SQL against docs/12-migrations.md; after applying in the upgrade fixture, compare affected row counts and the complete v3 delta for tenantId IS NULL and tenant clones. Confirm no production reseed is part of rollout.
  • Manual verification: none beyond the automated role, policy, and upgrade scenarios; there is no new response shape or UI implementation in this repository.
  • Documentation sync (same implementation change):
  • update docs/04-rbac.md preset matrices, role labels, policy inventory, management-editor semantics, and the one-time authoritative management-clone exception;
  • update docs/15-seeding.md with the named v3 migration exception while preserving the standing rule that ordinary reseeds do not propagate editable management presets;
  • update docs/19-attendance.md with HR/coordinator read reach and the department resolver/write tier;
  • update the selection-window/curriculum-selection guidance in docs/05-crud-patterns.md where those surfaces are catalogued;
  • update existing FE guides in place: docs/fe-guides/2026-08-05-referent-surface-FE-guide.md for referent/document access, docs/fe-guides/2026-07-28-attendance-module-FE-guide.md for attendance role/fence behavior, and docs/fe-guides/2026-08-03-session-view-switch-FE-guide.md for display labels. Put the permission-driven curriculum-selection affordance (canWrite plus target validation) in docs/04-rbac.md's frontend guidance because no dedicated management selection guide exists; do not create a *-BREAKING.md delta note;
  • update docs/REFERENCE.md entries/canonical-file pointers for every changed behavior.
  • Execution ownership: implementation plan must list targeted unit, E2E, migration-smoke, build, lint, and documentation checks. Per repository rules they run only when the user explicitly requests the gates; this design does not claim they have run.

12. Sign-off

  • Approved by: Fabio Barbieri
  • Date: 2026-08-20
  • Chat reference: original approval: "go on implementing the plan" after the Q1–Q10 resolution round; review correction approval: "fix all spec issues" — chat 2026-08-20. The correction makes rollout and effective authorization explicit without changing the approved workbook outcomes.

Until this section is filled, no implementation code is written. When you fill it, flip the frontmatter status: to Approved in the same edit.