T0-A Implementation Plan — Security and Authorization Foundation
| Item |
Value |
| Mode |
Server development (/home/scola/odoo) |
| Scope |
T0-U02, T0-U03 (cross-cutting only), STAB-005, STAB-007, STAB-008 |
| Docs read |
04-authorization-readiness.md, 08-stabilization-backlog.md, 09-release-quality-gate.md, T0 of 10-implementation-roadmap.md, AI guidelines |
| FE HEAD at plan |
scola-fe-v2 develop @ b48ca63859d3d822f22572dc76a08afbe4f2f653 |
| BE HEAD at plan |
gcgscola main @ 2ff6ac7cb41810bbc3b66821cdad004b52d1658a |
| Audit baseline |
FE 68b77d88…, BE 2ff6ac7… (2026-08-08) |
0. Phase 1 re-verification vs HEAD
| Finding |
Audit claim |
HEAD status |
Material change? |
| STAB-007 / AUTH-001 |
Role switch changes scola_role; permissions = union of all authoritative groups |
Confirmed. compute_permissions(user=…) still unions resolve_authoritative_role_codes_from_groups; switch_role docstring still states groups remain union; ir.http gates use union permissions |
FE SHA moved (68b77d88 → b48ca638); BE SHA unchanged. Defect logic unchanged. |
| STAB-008 / AUTH-003 |
First-prefix resolver can shadow specific rules |
Confirmed. resolve_api_route_access returns first startswith. Live probe: /api/v1/finance/fee-enrollments/* resolves to /api/v1/finance/; /api/v1/lms/modul-ajar/* resolves to /api/v1/lms/ |
None on defect. FE registry mirrors BE and uses same first-prefix algorithm. |
| STAB-005 / AUTH-009 |
Generic model+res_id mail-tracking with .sudo() |
Confirmed. core_mail_tracking_list accepts arbitrary model/res_id and queries via mail.tracking.value.sudo() |
None. Attachments already have model allowlist; mail-tracking does not. |
| AUTH-002 taxonomy |
.view/.manage collapse on optional domains |
Still present on payroll/kesiswaan/accounting etc. |
Out of T0-A domain refactor scope — inventory + classify only. |
| AUTH-004 deny-default |
Unregistered protected → None (no gate) |
Still true in ir.http._resolve_capability_gate |
Contained: improve resolver contract + tests; full protected-surface deny-default remains T0-U03/T1 dependency where route extractor ≠ registry. |
| AUTH-011–014 |
Audit/parity/provisioning/evidence |
Partial surfaces only |
Attribution on role-switch + denial logs in T0-A; full matrix deferred. |
Approved direction for this tranche (treated as RP-01 / RP-02 target architecture):
- Active role = server-enforced acting context (not UI persona).
- Effective capability = active role only (no inactive-role union).
- Route match = exact → longest specific prefix → approved fallback → deny.
- Fail closed over compatibility that weakens authority.
1. Current root cause
- STAB-007: Controller/session capability snapshot is derived from all Odoo groups mapped to roles, while
scola_role is only a persona label. FE can() prefers backend permission list, so UX and API both retain union authority after switch.
- STAB-008: Registry iteration order is load-bearing. Later, more-specific rules never win if an earlier broader prefix matches (fee-enrollments, modul-ajar).
- STAB-005: User-facing JSON endpoint elevates before proving model allowlist, native read rights, or company/domain ownership.
2. Target contract
Active role (T0-U02 / STAB-007)
| Concept |
Contract |
| Principal |
Authenticated res.users |
assigned_roles |
Eligibility only (authoritative group-backed) |
active_role / scola_role |
Exactly one acting context for request authority |
| Effective permissions |
ROLE_CAPABILITIES[active_role] (+ explicit CBT supplements only when active role is CBT-eligible) |
| Switch A→B |
Writes scola_role; payload permissions recompute for B only |
| Forged / unassigned |
Rejected by action_switch_active_scola_role (existing) + no capability inflation |
| Concurrent tabs |
One active role on user record; all sessions see same acting role after refresh/switch |
| Audit |
Principal login/uid + acting role + company on switch and capability denial |
Route resolver (T0-U03 / STAB-008)
Match order:
- Exact path rule (prefix equals normalized path, or path is exactly the prefix without requiring further segments when rule is exact-marked — implemented as: prefer rules where
normalized == prefix or normalized == prefix.rstrip('/')).
- Longest matching prefix among remaining rules.
- Explicit approved fallback list (empty initially / machine-listed).
- Otherwise
None (deny / no registry grant).
Collision policy: two distinct prefixes of equal length that both match the same path → validation failure (CI). Registry self-check + unit tests.
Contextual tracking (STAB-005)
Retain endpoint for product audit UI, but enforce:
- explicit model allowlist;
- action = list/read only;
- native
check_access_rights + browse/exists under user env before elevation;
- company/tenant filter when model has
company_id;
- safe field projection;
- identical denial for missing vs inaccessible (no existence leak);
- audit attribution for allowed reads.
3. Affected FE files
| File |
Change |
src/access/apiRouteAccessRegistry.js |
Longest/exact resolver; contract version bump; rule order may be normalized for readability |
src/stores/auth.store.js |
can() must not union inactive roles via multiRoleHasCapability when backend permissions present; switchRole already replaces permissions — tighten fallback to active role only |
src/access/accessContract.js |
Align fallback capability check with active-role-only |
tests/unit/access/* |
Resolver overlap tests; active-role store tests if present/needed |
docs/release-readiness/execution/* |
Plan, inventory, result |
No UI redesign / RoleSwitcher visual change.
4. Affected BE files
| File |
Change |
scola_core/services/auth_capabilities.py |
Active-role-only compute_permissions |
scola_core/controllers/auth.py |
Docs + switch payload/audit fields (active_role, principal, company) |
scola_core/models/ir_http.py |
Gate uses active-role permissions; denial log includes acting role |
scola_core/models/res_users.py |
Switch remains assignment-gated; ensure write does not re-inflate permissions via group sync side effects on capability snapshot |
scola_platform_support/api_route_access.py |
Deterministic resolver, collision validator, contract version, optional public allowlist export |
scola_core/controllers/core_reference_api.py |
Harden mail-tracking |
scola_core/tests/test_auth_capabilities_unit.py (+ new active-role suite) |
Multi-role / switch / forged / stale cases |
scola_platform_support/tests/test_api_route_access_unit.py |
Longest-prefix + overlap domains |
New: scola_core/tests/test_core_reference_mail_tracking_unit.py |
STAB-005 cases |
| New: registry validation helper used by tests / modular check if lightweight |
|
5. Migration / compatibility
- Transitional
.manage / .view capabilities remain in ROLE_CAPABILITIES and registry until domain tranches; inventory marks migration status.
- Multi-role users who relied on union permissions will lose inactive-role API authority after deploy — intentional (RP-01).
- Odoo model ACL groups for all assigned roles remain for ORM access eligibility (existing
_sync_role_groups); controller capability gate is the authoritative product boundary for /api/* registry paths. Residual ACL breadth is documented as downstream (STAB-017 / domain) risk, not reintroduced as capability union.
- CBT group supplements: applied only when active role is in
CBT_SUPPLEMENT_ACTIVE_ROLES to avoid parent+teacher union leaking CBT manager caps while acting as parent.
6. Session / token implications
- Active role persists on
res.users.scola_role (server-owned).
- No JWT claim change; session cookie + DB role.
- After switch, FE must use returned
permissions (already does); subsequent session/login payloads also recompute.
- Stale FE store: mitigated by backend recompute on every gated request; FE refresh after switch required for menu UX (existing). Tests cover stale FE permission list vs backend gate.
- Concurrent tabs: last successful switch wins globally for the user record.
7. Capability migration implications
- T0-A does not rewrite optional-domain controller gates.
- Inventory file lists each registry rule with current capability, target atomic action family, and status:
fixed_in_t0 | contained_not_baseline | downstream_tranche.
8. API-route-registry implications
- Contract version →
4.
- Resolver semantics change: longest prefix (fixes fee-enrollments, modul-ajar, any late-specific rules).
- Rule list order becomes non-load-bearing for correctness (still keep specific-before-broad for human readability).
- FE registry must stay byte-parity with BE (existing parity test).
- Unregistered
/api paths still return None (no grant); explicit public allowlist documented for auth/login/session style routes that are not capability-gated.
9. Audit implications
- Role switch: log uid, login, from_role, to_role, company_id, assigned_roles, outcome.
- Capability denial: log uid, active_role, company_id, path, capability.
- Mail-tracking allowed read: log principal, active_role, company, model, res_id, outcome.
- Full AUTH-011 mutation audit framework → deferred (downstream).
10. Tests to add/change
Active role
- Single-role teacher permissions == ROLE_CAPABILITIES['teacher']
- Multi-role teacher+homeroom with active teacher: no homeroom-only caps
- Active homeroom: homeroom caps present; switch back removes them
- Forged role rejected
- Stale scola_role without authority group → resolve_role_id fallback / empty unsafe grants
- Direct API gate uses active-role permissions (unit via compute + build_registry_capability_gate)
- Unauthorized switch
- Conflict-role case: two roles with disjoint exclusive caps; only active set granted
Route resolver
- Exact match wins over shorter prefix
- Longest prefix among overlaps
- library / fees / LMS / settings / attendance fixtures
- Equal-length collision detector
- Unknown path → None
- Public path policy explicit
Mail tracking
- Disallowed model; inaccessible record; nonexistent; cross-company; guessed ID; disallowed action; allowed use case; existence-leak comparison; audit hook invoked
FE
- Resolver parity + longest-prefix behavior
can() active-role-only fallback
11. Implementation units (reviewable)
| Unit |
Deliverable |
| U1 |
Plan (this file) |
| U2 |
STAB-007 compute_permissions + auth/ir_http/tests |
| U3 |
STAB-008 resolver FE+BE + overlap tests |
| U4 |
STAB-005 mail-tracking harden + tests |
| U5 |
Capability inventory |
| U6 |
QC run + T0-A-result.md |
12. Rollback strategy
- Revert BE commits touching
auth_capabilities.py, api_route_access.py, core_reference_api.py, ir_http.py, auth.py.
- Revert FE
apiRouteAccessRegistry.js + auth store/accessContract fallback.
- No schema migration in T0-A;
scola_role field unchanged → rollback is code-only.
- If production already deployed with active-role enforcement, rollback restores union permissions (security regression) — treat as emergency-only and re-hide multi-role if needed.
13. Explicit non-goals / stop conditions
- No Golden Tenant / T0-B, package suite split, navigation redesign, W01/W02/W05, optional-domain capability rewrites.
- Do not invent break-glass or domain ownership matrices.
- If a required product decision cannot be derived from approved direction above → record in result as open decision, do not invent policy.
14. Deferred classification preview
| Item |
Classification |
| STAB-007 active-role capability gate |
fixed in T0 |
| STAB-008 deterministic resolver |
fixed in T0 |
| STAB-005 mail-tracking |
fixed in T0 |
AUTH-002 domain .view mutations |
downstream tranche (inventory) |
| AUTH-004 full protected deny-default extractor parity |
contained / T0-U03 remainder + T1 |
| AUTH-005–010 domain scope |
downstream |
| AUTH-011 universal audit |
partial in T0; remainder downstream |
| AUTH-013/014 Golden Tenant evidence |
T0-B |
| Odoo ACL group union residual |
contained; STAB-017 / domain |