Skip to main content

RLS & tenancy gaps

Row-Level Security enforces membership but leaves several holes a rebuild must close.

  1. RLS does not enforce the workflow or role model. cases allows INSERT/UPDATE/SELECT to any company member (get_user_company_ids()); only is_company_admin is blocked from DELETE. No per-role, per-status, per-checklist write restriction. → the entire workflow engine is client-side and bypassable.

  2. companies is world-readable. A policy "Public can view companies by slug" FOR SELECT USING (true) was deliberately converted from RESTRICTIVE to PERMISSIVE with no TO role (migration 20260227212620). Anyone, including anon, can read all tenants' company rows — name, slug, branding, union_notification_email.

  3. Self-provisioning privilege escalation. companies INSERT WITH CHECK (true) + "users create own membership" WITH CHECK (user_id = auth.uid()), but the membership role is client-supplied and unconstrained — a user can insert themselves as super_admin/hr_admin. See self-provision escalation.

  4. Cross-tenant leaks in the UI queries. EvidenceVault queries evidence_items with no company_id filter ("Biblioteca global de evidência de todos os processos"); the Dashboard's pending-case_gates count has no company filter either. If RLS on those tables is loose, this is a live cross-tenant read.

  5. Storage bucket has no tenant isolation — see storage note.

  6. App-level role is global-max, not per-tenant — see role note.

Rebuild implication

Enforce the workflow and role model in the database / server (RLS + a transitions RPC). Scope companies read to a slug-lookup RPC. Constrain self-insert roles (server assigns the first role, e.g. always super_admin for the creator, never client-chosen). Add company_id filters everywhere and verify RLS on every table independently.