Verify Row-Level Security
Prove cross-tenant isolation, private storage, and role permissions before a pilot.
Kredal handles passports, Emirates IDs, and UBO documents. Before any pilot, confirm that a
workspace can only ever see its own data. This runbook is the manual security check from
TECH_SPEC.md §11.
1. Cross-tenant read isolation
- Create two accounts, A and B, each with their own workspace and one company.
- Copy company B's ID from B's URL (
/app/companies/<id>). - While signed in as A, navigate to
/app/companies/<company-B-id>. - Expected: a "not found" / error state, never B's data. RLS
selectpolicies are scoped tois_workspace_member(workspace_id), so the row is invisible to A.
Repeat for documents, questionnaire responses, assessments, and reports — all use the same workspace-membership check.
2. Private storage & signed URLs
- As A, upload a document, then open its preview. The URL is a short-lived signed URL (120s TTL) generated server-side.
- Copy that signed URL and open it after it expires (or strip the token). Expected: access denied. The bucket is private; there is no public path.
- Try to fetch B's storage path directly — the storage RLS policy checks workspace
membership on the first path segment (
{workspace_id}/...), so it is denied.
3. Role permissions (viewer is read-only)
- In a consultant workspace, add a member with the viewer role.
- As the viewer, confirm you can see a client's company but cannot upload documents, edit the company, add owners, or run an assessment.
- This is enforced by
is_workspace_writer()on all insert/update policies — viewers are deliberately excluded. See Security model.
4. Unauthenticated access
- Sign out and navigate to
/app. Expected: redirect to/login?next=/app(enforced bysrc/proxy.ts).
5. Audit log is append-only
Confirm audit_events has an insert policy but no update/delete policy — mutation is
denied by default. Company creation, uploads, deletions, assessments, and report generation
should each produce an audit row.
Automated backstop
The pure-logic layer (scoring, reports) is unit-tested with npm test. RLS itself is
verified manually as above; consider adding pgTAP tests when the pilot scales.