Direct GRANTs to anon that sidestep RLS thinking entirely
Policies get the attention; grants do the exposing. PostgREST can only serve what a role may access, and plain SQL GRANT statements — often issued years ago, or inherited from templates — decide that independently of any policy you have designed since.
Direct-grant auditing is not among RowShield's shipped rules; it is a roadmap candidate. Until then, this page supplies the queries that reconcile grants with the policy model you believe you have.
RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.
Why grants matter alongside RLS
With RLS enabled, grants define the ceiling of reachability and policies filter within it — a table granted to anon but policy-denied serves empty results, while a table granted narrowly but policy-open is unreachable despite generous policies. Misalignments in either direction produce confusion that wastes incident hours.
With RLS disabled, grants are the entire story: GRANT SELECT TO anon is a publication statement, full stop.
Mechanically, grants are access control list entries on the object: pg_class.relacl holds one record per grantee with the granting role recorded alongside. Default privileges form the template layer — ALTER DEFAULT PRIVILEGES stamps future objects with a chosen ACL at creation, which is how a schema stays consistent and how one forgotten template regresses a cleanup months later. The PUBLIC pseudo-role deserves a specific glance: a privilege granted to public reaches every role, including anon, regardless of what you believed you configured per role.
Dumping the effective matrix
has_table_privilege turns the abstract catalog into a straight yes/no grid per interesting role:
Extend the grid with routine checks where RPC endpoints exist — has_function_privilege answers the same question for callable functions that table grants answer for data.
SELECT c.relname AS table_name,
has_table_privilege('anon', c.oid, 'SELECT') AS anon_select,
has_table_privilege('anon', c.oid, 'INSERT') AS anon_insert,
has_table_privilege('authenticated', c.oid, 'SELECT') AS auth_select,
has_table_privilege('authenticated', c.oid, 'UPDATE') AS auth_update
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p') AND n.nspname = 'public'
ORDER BY c.relname;Reading it against intent
Walk the grid with your policy design in hand. Anon SELECT true on user-data tables is a finding regardless of policies, because it signals the RLS-disabled escape hatch or a deny-all surprise waiting in staging. Unexpected INSERT grants explain mysterious rows; missing UPDATE grants explain mysterious failures.
Also inspect default privileges — objects created later inherit the grant template, which is how one cleanup regresses next sprint:
SELECT defaclrole::regrole AS granting_role,
defaclnamespace::regnamespace AS in_schema,
defaclobsdefacl, defacltype
FROM pg_catalog.pg_default_acl;Roadmap honesty and interim practice
A GRANT_TO_ANON rule is planned, with fixtures drafted and no ship date promised. Interim practice: rerun the matrix after every external contribution, template change, or restore, and treat any drift between grants and your documented intent as a finding even before a detector automates the opinion.
Variations you will meet
Sequence grants break writes quietly: an INSERT grant on a table whose identifiers draw from a sequence the role cannot use fails at write time with an unhelpful permission error. Function EXECUTE grants define the RPC surface — PostgREST exposes any function in an exposed schema that the presenting role may execute — so grant hygiene on routines matters as much as on tables. Column-level grants add nuance too: a table-level SELECT revoked while column grants persist keeps specific columns readable.
Restores and environment clones carry grants with them: a database refreshed from a stale template arrives with the template's matrix intact, and corrections made only in production vanish on the next refresh unless they exist in the source.
Common false leads
Grant work has its own folklore. Revoking from anon does not cascade anywhere useful: column grants and routine grants persist independently, so a table-level REVOKE can leave a readable remainder behind. Granting to authenticated does not imply anon, nor the reverse — they are unrelated principals. And grants never substitute for policies on RLS-enabled tables: tightening grants cannot compensate for a permissive policy any more than policies can exceed their grant ceiling.
Column-level grants create readable remainders that table-level thinking misses entirely: revoke the table privilege, and a granted column can survive through its own entry. When narrowing, enumerate both layers in the same change.
Frequently asked
- Do grants matter if RLS is enabled everywhere?
- Yes — grants bound reachability and explain empty-versus-forbidden behaviour. With RLS on, the grant is the ceiling and policies filter within it; when the ceiling sits lower than policy intent, users see denials that look like policy bugs. Audits keep the ceiling aligned with what the policies promise.
- Who grants these by default?
- Platform defaults and inherited templates do most of it, which is why a fresh project and a year-old one differ. Provenance is interesting but secondary: the matrix reflects what exists now, and drift from your documented intent is the finding regardless of who issued the grant.
- Will Revoking grants break PostgREST?
- Revoking privileges your frontend never exercises is safe and clarifying, provided you check routine grants and sequences on the same paths before assuming so. Verify behaviourally afterwards — exercise the affected routes with the real keys — rather than trusting the catalog change alone.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit