Row-Level Security in Cinderblock
Every table in the public schema has RLS enabled, with policies that spell out both USING (read/visibility) and WITH CHECK (write) — no implicit defaults. The tests/05_security_definer.sql hardening suite asserts the catalog state stays this way:
- Every public-schema table has
rowsecurity = on. - Every public view is
security_invoker = on(asecurity_definerview bypasses the caller's RLS). - No policy uses
using(true)orwith check(true)unless it's scoped to a single non-public role. - Every
app_private.*helper hassearch_path = ''inproconfig.
Helper functions (all security_definer, search_path = '')
is_workspace_member(_workspace_id uuid) -> boolean
has_workspace_role(_workspace_id uuid, _min_role workspace_role) -> boolean
workspace_is_writable(_workspace_id uuid) -> boolean
user_has_mfa(_user_id uuid) -> boolean
is_slug_reserved(_slug text) -> booleanEvery helper has explicit set search_path = '' — without it, a workspace member who creates a function in their own schema named auth.uid() could hijack identifier resolution. The search-path attack test in tests/05_security_definer.sql simulates this end-to-end and asserts the helper still resolves the real auth.uid().
Policy patterns by table
workspaces
select: deleted_at is null and is_workspace_member(id)
insert: created_by = auth.uid() and deleted_at is null
update: has_workspace_role(id, 'owner') (USING + WITH CHECK)
delete: <no policy — hard-delete via service-role cron only>workspace_members
select: (user_id = auth.uid() and removed_at is null)
or has_workspace_role(workspace_id, 'admin')
insert: with check (false) -- service-role only via invite-accept
update: has_workspace_role(workspace_id, 'admin')
delete: <no policy — soft-delete sets removed_at>audit_events
select: admin+: all rows
member: actor_id = auth.uid() only
guest: none
insert: only via cb_audit_writer role (separate INSERT-only grant)
update/delete: <no policy, no grant — append-only by design>tasks
select: is_workspace_member(workspace_id)
insert: has_workspace_role(workspace_id, 'member')
and created_by = auth.uid()
and workspace_is_writable(workspace_id)
update: has_workspace_role(workspace_id, 'member')
and workspace_is_writable(workspace_id)
delete: has_workspace_role(workspace_id, 'admin')The hostile fixture
tests/01_fixture.sql seeds 5 workspaces × 8 users with a deliberate membership matrix that mixes overlapping memberships across workspaces, role variants, and one outsider. Every test authenticates as a user with no business reading the target row and asserts an empty result.
See the live policy viewer for the deployed catalog state, or the latest test results for the green run.