# Role Model — proposal & current implementation

Follow-up to the [July 2026 security audit](security-audit-2026-07.md), finding **A2**
(broken access control).

**Status: IMPLEMENTED.** David confirmed `driver`/`fitter` are field staff, so the map
below is enforced by a **central default-deny in `CRUD/session.php`**: a non-manager role
may only reach the field-capture allowlist (`portal_field_role_may_access()` in
`CRUD/auth.php`); everything else is manager-only. Managers and local dev are unaffected.
The tiers below are the model; the allowlist in `auth.php` is the source of truth. Open
per-role refinements (which specific lookups each field role needs) can still be tuned —
see "Open decisions".

## Current reality

The three roles (`manager`, `driver`, `fitter`) exist in the `users` table and in login,
but **authorisation was never enforced**:

- The navbar shows every link to everyone; there is no role routing on login or the main
  page; there is no `driver`/`fitter` branching anywhere in the code.
- `users_page_permissions` held only 3 rules, all pointing at paths that don't exist.
- So every logged-in user could reach ~all 870 pages, regardless of role.

**Already fixed in the audit:** `users/*` (account management — was a privilege-escalation
path) and the financial modules `customers/debtors/*` and `prices/*` are now
`require_role('manager')`.

## The one question that decides everything

**Are `driver` / `fitter` accounts actually used by field staff (e.g. on tablets to submit
PODs / PDIs / timesheets), or are they office staff who happen to have that role?**

- If **office staff** → they're trusted with everything; the per-module lockdown below is
  largely unnecessary, and we're essentially done (users + financial already guarded).
- If **field staff** → they should be confined to the capture flows, and the map below
  applies.

The rest of this proposal assumes **field staff** (the stricter, safer reading).

## Proposed map

### Tier 1 — Manager only (`require_role('manager')`)
Everything commercial, administrative, configuration, or compliance:

`customers` · `prices` ✓ · `customers/debtors` ✓ · `suppliers` · `invoice_review` ·
`reports` · `hire_list` · `history` · `site_move` · `missing_info` · `site` ·
`plant_list` · `plant_specifications` · `category` · `category_type` · `job_type` ·
`delivery_type` · `additions` · `holiday_dates` · `contacts` · `messages` · `maps` ·
`drivers` (driver **records**/HR, incl. infringements) · `fleet` · `ghg` · `iso` ·
`H&S` · `hs` · `complaints` · `audit_history` · `uploads` · `users` ✓

(✓ = already guarded.)

### Tier 2 — Field capture (`manager` + the relevant field role)
The submission flows and what they need:

| Area | Roles | Notes |
|---|---|---|
| `forms/pod/*` | manager, driver | POD capture/submission |
| `forms/pdi/*` | manager, fitter | PDI capture/submission |
| `forms/fitters_report/*`, `fitter/*` | manager, fitter | Fitter's reports |
| `forms/timesheets/*` | manager, driver, fitter | Timesheet capture |

### Tier 3 — Shared (any authenticated user)
Infrastructure/assets, not data pages: `main` (landing), `nav_bar`, `functions`, `img`,
`js`, and the login/session plumbing.

## Open decisions (need David)

1. **The pivotal question above** (field staff vs office staff).
2. If field staff: do the capture forms need **read** access to any Tier‑1 data to work —
   e.g. a customer/site/plant/hire lookup to pick a job? If so, those specific lookup
   endpoints move to Tier 2 (read-only) rather than manager-only. (A quick check of
   `pod_selector_driver.php` / `timesheet_create.php` showed no obvious heavy lookups, but
   this needs confirming against the real driver workflow.)
3. Confirm the Tier‑2 split: driver = PODs + timesheets; fitter = PDIs + fitter reports +
   timesheets.

## How it gets applied (once approved)

Add one line — `require_role('manager')` (Tier 1) or `require_role('manager','driver')`
etc. (Tier 2) — after the `session.php` include on each page, exactly as done for
`users/*` and the financial modules. Local dev and manager access are unaffected; the
guard only redirects wrong-role users on live. Each module is verified the same way:
`require_role()` unit cases + an HTTP regression that the pages still load locally.
