Lewati ke isi

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 (68b77d88b48ca638); 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

  1. 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.
  2. STAB-008: Registry iteration order is load-bearing. Later, more-specific rules never win if an earlier broader prefix matches (fee-enrollments, modul-ajar).
  3. 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:

  1. 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('/')).
  2. Longest matching prefix among remaining rules.
  3. Explicit approved fallback list (empty initially / machine-listed).
  4. 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

  1. Revert BE commits touching auth_capabilities.py, api_route_access.py, core_reference_api.py, ir_http.py, auth.py.
  2. Revert FE apiRouteAccessRegistry.js + auth store/accessContract fallback.
  3. No schema migration in T0-A; scola_role field unchanged → rollback is code-only.
  4. 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