Skip to content

Add as_anonymous() helper for testing RLS as the anon role #78

@alialavia

Description

@alialavia

Summary

Add a small as_anonymous(db) context manager (or, if simpler, document the recipe pattern) for testing RLS behavior as the anon Postgres role with no JWT claims. The existing as_rls_user(db, user_id, role="anon") mostly works but sets a sub claim that anon users shouldn't have, which can hide bugs where a policy implicitly trusts the presence of sub.

Motivation

Several RLS bug classes only surface when the requester is genuinely anonymous:

  • Anon falling through a policy because auth.uid() returns NULL and the policy expression doesn't handle NULLs explicitly (auth.uid() = user_id evaluates to NULL → false silently, returning an empty array; common cause of "the API works but returns nothing").
  • Tables where the developer forgot to enable RLS, so anon's default SELECT grant on public.* exposes the table.
  • Policies whose USING clause implicitly trusts request.jwt.claims->>'sub' being present (rather than handling it being NULL).

Today the test author writes something like:

db.execute("SET LOCAL ROLE anon")
try:
    rows = db.query("SELECT * FROM tickets")
finally:
    db.execute("RESET ROLE")

That works but isn't discoverable, is easy to forget the RESET, and doesn't compose with the existing as_rls_user style.

Proposed API

from sqlproof.contrib.supabase import as_anonymous

with as_anonymous(db):
    rows = db.query("SELECT * FROM tickets")
assert rows == []  # if RLS is correctly enforcing

Implementation: ~15 lines, mirrors as_rls_user's structure (savepoint-friendly, exception-safe, RESET ROLE on exit). Crucially, it does not call set_config('request.jwt.claims', ...) — so auth.uid() truly returns NULL inside the block, modeling real anonymous requests rather than authenticated-as-anon.

Alternative: docs-only

If a new helper feels like API-surface bloat, document the raw SET LOCAL ROLE pattern in guides/supabase.md with an explicit callout about "why you can't just pass role='anon' to as_rls_user".

Related

  • Inbox sample design spec — see "Gap 2" in the RLS bug-classes analysis: docs/superpowers/specs/2026-06-03-inbox-sample-design.md
  • src/sqlproof/contrib/supabase.py — existing as_supabase_user, as_rls_user

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions