Row-level security
PostgreSQL row-level security is a policy attached to a table. Besu records policies — so
besu pull writes them down and a snapshot round-trips them — but it never creates or drops one.
A policy is written in a migration, where its raw SQL is reviewed like any other change.
A policy applies to a role or to PUBLIC, and FORCE ROW LEVEL SECURITY makes it apply to the
table's owner too. Both are ordinary SQL inside the same migration.
What is modelled
Schema::policies is a list of Policy values:
PostgreSQL introspection reads pg_policies, so a besu pull snapshot names every policy exactly
as it exists. besu generate and push ignore the list in both directions.
Why the differ ignores a policy
A USING clause is raw SQL, and comparing two of them reliably would mean parsing PostgreSQL's
own output. Ignoring the list is the safe half of management: a push that was never told about
a policy cannot drop it, and a snapshot that records one documents the database without claiming
push will recreate it.
The same reasoning applies to ENABLE ROW LEVEL SECURITY, but there is one direction Besu can
safely take:
- A table marked
rlsin the declaration, or#[besu(rls)]on a model, getsALTER TABLE ... ENABLE ROW LEVEL SECURITYwhen it is created or when the flag first appears. - RLS is never disabled automatically, even when the declaration does not mention it. Disabling RLS silently removes the guarantee every policy on the table depends on, so it stays a migration.
The policy itself is still written by hand:
Gotchas
- Run migrations as the right role. A superuser, or a role with
BYPASSRLS, ignores policies, so a migration that tests them can appear to pass while a normal role is denied. ENABLEis notFORCE. The table owner bypasses policies unlessFORCE ROW LEVEL SECURITYis set.- There is no
CREATE POLICY IF NOT EXISTS. Re-running a policy migration by hand fails; the journal is what stops it running twice. - A view over a protected table runs as its owner by default. On PostgreSQL 15 and later,
set
security_invoker = trueon the view; before that, filter inside the view. - Removing a table removes its policies.
DROP TABLEtakes them with it, andDROP COLUMNfails while a policy names the column.
