Role-matrix audit runbook
1. Intro
This runbook drives a reproducible manual sweep of every role declared in
docs/PERMISSIONS.md §3 against the live app. The
goal is to validate that the deployed permission matrix matches real user
experience — i.e. every role sees exactly the pages / sections / buttons
§5 promises, and nothing more. Automated tests
(src/test/permissions.matrix.test.ts) catch structural drift between
code and PERMISSIONS.md, but they cannot catch behavioural drift:
conditional UI that renders a button the matrix says should be hidden, a
route guard that permits a role it shouldn't, or a server handler missing
requirePermission. Those only show up when a human logs in as each
role and clicks through.
Run this sweep any time docs/PERMISSIONS.md changes materially — a new
role added, a role renamed, a permission redistributed, or a new page /
section gated. It is not a gate on every PR (570+ unit tests already
cover that) — treat it as a quarterly-or-on-material-change audit.
The app is currently pre-production (no real customer data), so we drive the sweep with disposable test accounts seeded directly via the Supabase Admin API.
2. Setup
2.1 Seed the test users
You'll need the service-role key for your dev Supabase project. Export
the env vars (see env.example for the full list) and run:
# Required
export SUPABASE_URL="https://<your-project>.supabase.co"
export SUPABASE_SERVICE_ROLE_KEY="<service-role jwt>"
# Optional — defaults are fine
# export TEST_USER_PASSWORD='AlhudaTest!2026'
# export TEST_USER_EMAIL_DOMAIN='test.alhuda.local'
npm run seed:test-users # 17 internal staff roles
npm run seed:test-portal-users # AGENT (partner) + CUSTOMER
Both scripts are idempotent — re-running is safe and will reset passwords
+ refresh metadata for any test user that already exists. If you add a
new role to docs/PERMISSIONS.md §3, re-run the staff script and it
will pick it up; removed roles leave stale test users behind (delete
those manually via the admin UI if you care).
The scripts print a summary table at the end. Paste the relevant row into §2.2 below so the auditor always has the latest credentials.
2.2 Test-user credentials
- Login URL (staff):
/auth - Login URL (partner portal):
/partner/auth - Login URL (customer portal):
/customer/auth - Shared password: value of
TEST_USER_PASSWORD(defaultAlhudaTest!2026)
| Role | |
|---|---|
| CEO | ceo@test.alhuda.local |
| GM | gm@test.alhuda.local |
| IT_ADMIN | it-admin@test.alhuda.local |
| ADMIN_HR | admin-hr@test.alhuda.local |
| SALES_MANAGER | sales-manager@test.alhuda.local |
| SALES_EXEC | sales-exec@test.alhuda.local |
| B2B_MANAGER | b2b-manager@test.alhuda.local |
| B2B_EXEC | b2b-exec@test.alhuda.local |
| OPS_MANAGER | ops-manager@test.alhuda.local |
| OPS_EXEC | ops-exec@test.alhuda.local |
| FINANCE_MANAGER | finance-manager@test.alhuda.local |
| ACCOUNTANT | accountant@test.alhuda.local |
| CASHIER | cashier@test.alhuda.local |
| TICKET_MANAGER | ticket-manager@test.alhuda.local |
| VISA_OFFICER | visa-officer@test.alhuda.local |
| AUDITOR | auditor@test.alhuda.local |
| AGENT (portal) | agent@test.alhuda.local |
| CUSTOMER (portal) | customer@test.alhuda.local |
These emails assume the default
TEST_USER_EMAIL_DOMAIN=test.alhuda.local. If you set a different domain the pattern is<role-lowercased-with-dashes>@<domain>.
3. Audit procedure — per role
For each role below, log in with the credentials from §2.2 and walk through each bucket. Check items in the Findings list as you confirm them; leave a note on anything that doesn't match.
Cross-reference against docs/PERMISSIONS.md §5
(role grants) and §6 (page-by-page action matrix) — that document is
authoritative; if you find a conflict between this runbook and
PERMISSIONS.md, PERMISSIONS.md wins and this runbook should be
updated.
3.1 CEO / GM / IT_ADMIN (super-admins)
All three behave identically — seeded every permission via
migration 20260416020000. Audit any one (spot-check the others with a
single page each).
Should see: - Every sidebar item: Dashboard, Inventory (Airline Blocks, FIT, B2B, Ground, Hotels, Visa), Sales (Leads, Requests, Quotations, Bookings), Groups, Finance, Customers, Business Partners, Suppliers, People, Workflow (Ops / Corrections / Finance / Tickets / WhatsApp / Communications / Internal Chat), Reports, Admin. - Every create / edit / delete button, including destructive ones (delete booking, delete hotel, delete supplier). - Admin → Permissions matrix page (edit allowed). - Finance → maker-checker override buttons (approve-own / reverse-own).
Should NOT see: nothing hidden — this is the bypass tier.
Workflows to exercise:
- Reopen a closed accounting period (Finance → Settings → Periods).
Verifies finance.periods.unlock works for super-admins even with no
role-name shortcut.
- Approve a journal you created yourself (maker-checker override).
Confirm the button is visible and succeeds — this distinguishes
super-admin from everyone else.
- Delete a custom role from Admin → Permissions. Non-super-admins must
not be able to do this (admin.roles.delete).
Findings: - [ ] All sidebar items visible - [ ] Every destructive action button visible - [ ] Can reopen closed period - [ ] Approve-own journal works - [ ] Delete custom role works
3.2 ADMIN_HR
Should see:
- Every staff page (bookings, finance, groups, hotels, etc.) — ADMIN_HR
is seeded every permission EXCEPT finance.journals.approve_own and
finance.journals.reverse_own.
- Admin → Permissions matrix, Users, Audit.
Should NOT see: - Approve-own-journal / reverse-own-journal buttons on a journal they created. The UI should hide those; the API must reject them.
Workflows to exercise: - Create a manual journal → try to approve it yourself → expect block with maker-checker error. - Create a staff user and assign a role. Reset their password. Delete them.
Findings: - [ ] Can access all staff pages - [ ] Approve-own-journal blocked with clear error - [ ] User management full CRUD works
3.4 SALES_MANAGER
Should see: - Sidebar: Dashboard, Sales (Leads, Requests, Quotations, Bookings), Groups, Customers, Reports. - Full CRUD on bookings. - Create / edit / delete customers. - Create quotations; create groups. - Group pricing edit; group invoice create / edit.
Should NOT see:
- Finance module (finance.view not granted).
- Admin page (admin.view not granted).
- Business Partners page (agents.view not granted).
- Suppliers, Hotels, Inventory, Visa, Tickets modules.
- Issue / cancel group invoice buttons (those are FINANCE_MANAGER-only).
Workflows to exercise:
- Create a quotation → convert to booking → submit for approval →
confirm Sales cannot approve its own submission.
- Create a group → edit pricing → create a draft invoice → verify
"Issue" button is disabled or hidden (requires group_invoices.issue).
Findings: - [ ] Finance sidebar item hidden - [ ] Admin sidebar item hidden - [ ] Cannot issue group invoice - [ ] Cannot approve own booking submission
3.5 SALES_EXEC
Should see: same as SALES_MANAGER except no customer-delete, no group-create, no reports-export, no group-pricing/invoice-edit.
Should NOT see: - Customer delete button (customers.delete is manager-only). - Group create button. - Export buttons on reports. - Group pricing edit, invoice create/edit.
Workflows to exercise:
- Create a booking → add passengers → add payment (expect
finance.create gate; SALES_EXEC doesn't have it so the "Add
Payment" button should be hidden).
- Try to delete a customer; expect no delete button.
Findings: - [ ] Cannot delete customer - [ ] Cannot create group - [ ] Cannot add payment on booking - [ ] Cannot export reports
3.6 B2B_MANAGER
Should see: - Business Partners (Agents) list + full CRUD. - Bookings full CRUD, customer create, quotations. - Reports with export. - Group pricing edit, invoice create/edit.
Should NOT see: - Finance module. - Admin module. - Suppliers, Hotels, Inventory. - Issue / cancel group invoice.
Workflows to exercise: - Create a new business partner with a login → log out → log in as that partner on the partner portal → verify isolation (sees only own data). - Edit partner commission rate; verify the field saves.
Findings: - [ ] Partner CRUD works - [ ] Partner login provisioning works end-to-end - [ ] Cannot access Finance
3.7 B2B_EXEC
Should see: Partners list (view only), bookings full CRUD, customer create, quotations.
Should NOT see: Partner create / delete, finance, reports export, group invoice actions beyond view.
Findings: - [ ] Partner list view-only (no Add button) - [ ] Bookings CRUD works - [ ] Finance module hidden
3.8 OPS_MANAGER
Should see: - Sidebar: Dashboard, Inventory (all subsections), Hotels, Groups, Workflow (Ops Approvals), Reports, Suppliers. - Approvals queue — can approve/reject ops-tier. - Suppliers full CRUD. - Hotels full CRUD; Inventory full CRUD. - Group create.
Should NOT see:
- Sales menu (bookings/customers/quotations/leads).
Note: bookings.view is granted, so they may see a bookings link —
if so, confirm list-view is the only thing they can do (no create,
edit, delete).
- Finance module.
- Admin module.
- Business Partners (agents).
Workflows to exercise:
- Approve an ops-submitted booking → verify it moves to finance stage.
- Create a hotel → assign it to a group → record a supplier payment
(expect gate: finance.create not granted, so payment flow blocked).
Findings: - [ ] Ops approvals queue works - [ ] Suppliers CRUD works - [ ] Cannot record supplier payment directly (Finance-only)
3.9 OPS_EXEC
Should see: Inventory view + edit, Hotels edit + export, Suppliers edit. Bookings view.
Should NOT see: Create anything from scratch (no create permissions except inventory.edit which covers some update flows); no approvals queue; no finance; no admin.
Findings: - [ ] Inventory view + edit works - [ ] Approvals queue hidden - [ ] Admin hidden
3.10 FINANCE_MANAGER
Should see: - Finance module — every tab (Accounts, Payments, Vouchers, Journals, Approvals, Cancellations, Reports, Settings, TDS). - Workflow → Finance Approval queue. - Group invoices: create, edit, issue, cancel. - Reports with export. - Bookings (view only).
Should NOT see: - Approve-own-journal / reverse-own override (super-admin only). - Admin module. - Sales create / edit pages.
Workflows to exercise: - Create a journal → ask ADMIN_HR to approve it → verify it posts to Day Book. - Try to approve a journal you created yourself → expect maker-checker rejection with a clear message. - Issue a draft group invoice → verify revenue + tax journal posts. - Cancel an issued invoice → verify reversing credit-note journal posts.
Findings: - [ ] All finance tabs visible - [ ] Approve-own journal blocked - [ ] Group invoice issue works - [ ] Group invoice cancel works (posts credit-note)
3.11 ACCOUNTANT
Should see: - Finance module (Accounts, Payments, Vouchers, Journals, Reports, Settings, TDS). - Group invoices: create, edit (no issue, no cancel). - Reports with export. - Bookings view.
Should NOT see:
- Finance → Approvals tab approve/reject buttons (approvals.approve
not granted — ACCOUNTANT can view but not act).
- Group invoice issue or cancel buttons.
- Reverse-own / approve-own journal buttons.
- Finance → Profit & Loss report (sectional permission
finance.reports.profit_loss.view not granted — see PERMISSIONS.md
§4.4 and §8 item 10 — this is the canonical "Accountant can see Trial
Balance but not P&L" example).
Workflows to exercise: - Record a payment → verify it lands in the approvals queue for a finance manager to act on. - Visit Finance → Reports → confirm P&L tab hidden, Trial Balance tab visible.
Findings: - [ ] Finance tabs visible but Approvals actions hidden - [ ] Group invoice issue/cancel hidden - [ ] P&L report hidden, Trial Balance visible
3.12 CASHIER
Should see: - Finance → Payments / Vouchers (only). - Bookings view.
Should NOT see:
- Accounts, Journals, Approvals, Reports, Settings, TDS — CASHIER has
finance.create + finance.payments.record only; no finance.edit,
no approvals.approve, no reports.export.
- Anything outside Finance (no Sales, no Admin, no Inventory).
Workflows to exercise: - Record a cash receipt against an agent → verify it posts but remains pending approval. - Try to access Finance → Accounts → expect 403 or redirect.
Findings: - [ ] Payments / Vouchers tabs visible - [ ] All other Finance tabs hidden - [ ] Sidebar shows only Finance + Bookings
3.13 TICKET_MANAGER
Should see: Tickets queue, ticket detail, sync tickets, bulk update names, request exception, approve exception, issue ticket. Reports with export. Bookings view.
Should NOT see: Everything else (no Sales, no Finance create, no Admin).
Workflows to exercise: - Bulk-update passenger names on a batch of tickets. - Request an exception → approve it → issue ticket → verify status transition.
Findings: - [ ] Tickets queue works - [ ] Approve exception + issue ticket both work - [ ] No Finance create capability
3.14 VISA_OFFICER
Should see: Visa pipeline, visa case detail, create case, update status, upload docs, delete visa docs. Bookings view.
Should NOT see: Finance, Sales create, Admin, Inventory.
Workflows to exercise: - Create a visa case for a passenger on a booking → upload passport → move through statuses → verify history log.
Findings: - [ ] Visa CRUD works - [ ] Bookings list visible but no edit - [ ] All other modules hidden
3.15 AUDITOR
Should see: Every *.view page (read-only across the board).
Reports with export.
Should NOT see: Any create / edit / delete button on any page.
Workflows to exercise: - Walk through every sidebar item. For each page, confirm the header has no "New / Add / Create" button and no row-level edit or delete. - Export a report → verify it downloads.
Findings: - [ ] Every page loads read-only - [ ] No create/edit/delete buttons anywhere - [ ] Report export works
3.16 AGENT (partner portal)
Log in at /partner/auth.
Should see:
- Partner dashboard with own KPIs only.
- Own bookings, own customers, own quotations.
- Partner wizard to create a booking (always on_account payment).
- Own invoices list; create / edit / print own invoice.
- Own reports (bookings, revenue, customers, invoices).
- Profile (edit contact person, manage sessions).
Should NOT see:
- Any staff page (/app, /sales, /finance, /admin redirect away).
- Other agents' data — ownership is enforced by the API.
Workflows to exercise:
- Create a booking via the partner wizard → confirm it shows up only
for this agent.
- Log in as a second agent (provision another via the admin UI) →
verify you cannot see the first agent's bookings.
- Try navigating directly to /app → expect redirect to partner
dashboard.
Findings:
- [ ] Partner dashboard loads
- [ ] Own data isolation holds
- [ ] Staff routes redirect away
- [ ] Wizard forces on_account payment method
3.17 CUSTOMER (customer portal)
Log in at /customer/auth.
Should see: - Own bookings, own invoices (download), own visa status / docs, own statement, own profile. - Accept / reject quotations sent to them. - Create / respond to service requests. - Upload payment proof with receipt.
Should NOT see: - Any staff page. - Other customers' data.
Workflows to exercise: - Open a quotation sent by staff → accept it → verify a booking is created and shows in Own Bookings. - Upload a payment proof against an invoice → verify it enters the finance approvals queue on the staff side.
Findings: - [ ] Customer dashboard loads - [ ] Quotation accept creates booking - [ ] Payment upload reaches staff approvals - [ ] Staff routes redirect away
4. Known drift
docs/PERMISSIONS.md §8 already tracks the structural drift items that
the automated drift test cannot catch (because the mismatch is a
deliberate not-yet-wired placeholder rather than a bug). Read §8 before
filing anything from this sweep, in case the issue is already
documented. Hot list:
/correctionsusesbookings.view(should becorrections.view)./communicationsand/whatsappusecustomers.view(should becommunications.view)./suppliersusesinventory.view(should besuppliers.view).- Several
*.delete/*.allocatepermissions are RLS-enforced but not UI-gated yet, so buttons remain visible. admin.users.*granular permissions exist in §4.4 but are not yet split fromadmin.view./appdashboard usesallowedRolesinstead ofdashboard.view.- Granular §4.4 permissions are seeded but mostly unwired; only the finance report button-filter currently honours them.
If the behaviour you observe matches one of these, note it against the existing §8 entry instead of opening a new issue.
5. How to file findings
For each unchecked or failing item above that is NOT already in §8:
- Open a GitHub issue with the title prefix
[role-audit]. - Tag it with the
permissions-driftlabel. - In the body include:
- Role affected (e.g.
ACCOUNTANT) - Expected behaviour (quoting the relevant row from
docs/PERMISSIONS.md§5 / §6) - Observed behaviour (what actually happened)
- Reproduction steps (login → page → action)
- Screenshot if the drift is a visible button that shouldn't render
If the fix involves changing the matrix itself, the issue becomes a
PR against docs/PERMISSIONS.md + the corresponding seed migration
under supabase/migrations/ + any <PermissionGate> / route-guard /
requirePermission code references. Run npm test before merging —
src/test/permissions.matrix.test.ts will fail otherwise.