Skip to content

feat(w1): ReBAC core — check() with reason paths, barriers, Postgres store#21

Merged
geminimir merged 1 commit into
mainfrom
w1/rebac-core
Jul 16, 2026
Merged

feat(w1): ReBAC core — check() with reason paths, barriers, Postgres store#21
geminimir merged 1 commit into
mainfrom
w1/rebac-core

Conversation

@geminimir

Copy link
Copy Markdown
Owner

Summary

Milestone W1 — the real ReBAC engine. Closes #4, #5, #6.

The differential harness introduced in W0 (currently xfail) becomes a real gate: core/rebac.py is judged against core/oracle.py on 5,000 randomly generated authorization graphs per CI run, across three properties (safety, fidelity, pointwise).

What ships

  • core/store.pyTupleStore Protocol + InMemoryStore. Deliberately different data shape from the oracle (adjacency dict vs. linear iteration) so the two cannot share bugs.
  • core/rebac.py — the engine. Depth-limited (MAX_DEPTH=8), cycle-safe DFS (oracle uses BFS → different algorithm, different failure modes). Barrier check first, short-circuits. Returns (bool, ReasonPath) — the reason path is what will make W3's audit log usable.
  • sql/001_init.sql + core/postgres_store.py — Postgres schema (tuples + barriers, fwd/rev indexes, expiry, CHECK against self-side barriers) and a full store implementation. group_memberships uses a recursive CTE — one round trip regardless of nesting depth.
  • tests/test_rebac.py — 18 handwritten fixtures targeting DFS-specific traps: longer path chosen when shorter is expired, diamond backtracking, self-loops, mutual cycles, off-by-one at exactly MAX_DEPTH, deep-grant barriers, reason-path chain integrity.
  • tests/test_postgres_store.py — 7 integration tests (CRUD, idempotency, expires_at round-trip, end-to-end rebac.check() against real Postgres including barriers).
  • docker-compose.yml + CI with a Postgres service container.

Bug caught by the harness on first run

The differential harness immediately found a bug — in the generator, not the engine. Hypothesis produced graphs with two Document objects sharing the same id, which is illegal in the real schema (id is a PK). Oracle and engine happened to pick different rows via nondeterministic frozenset iteration, producing a spurious disagreement. Fixed in evals/generators.py by enforcing unique doc IDs; the harness has been doing its job since commit one.

Test plan

  • make oracle — 15 tests green.
  • make rebac — 18 tests green.
  • make test — 41 tests green (7 Postgres tests skipped without WARDEN_TEST_DB_URL).
  • make differential-gate (5000 examples) — 3 properties green in ~1m43s locally.
  • make postgres-integration — 7 tests green against a real Postgres 16 container.
  • CI: three jobs pass on push (see workflow run).

Invariant checklist

  • make oracle green.
  • make test green.
  • Differential harness — xfail marker removed, tests are truly green.
  • No shared code between core/oracle.py and core/rebac.py. Different algorithms (BFS vs. DFS), different data structures (linear iteration vs. adjacency dict), different barrier evaluation code paths.
  • Gate 2 semantics: barrier deny short-circuits before grant search. Tested end-to-end in fixtures and property tests.
  • Permissive-superset invariant — not applicable to W1 (that's a W2 concern; landing there).

Security-sensitive?

  • This PR touches core/algebra.py (no), core/oracle.py (no), core/rebac.py (yes, new), core/barriers.py (folded into rebac), core/labels.py (W2), gate wiring (W3).

Yes — this IS the authorization engine. That's why the 5000-example differential gate exists and is required for CI to pass.

  • I have read the invariants section of the README.
  • This is not an undisclosed vulnerability fix.

Notes for reviewers

  • _first_blocking_barrier returning None for non-doc objects is intentional (see comment). Barrier tags currently only attach to documents; container-level inheritance is a W2 concern via label materialization.
  • The _doc_tags side channel on PostgresStore is a W1-only shim — in W2 the store will read barrier tags from the documents table. Documented in place.
  • expand() uses store._outgoing directly for InMemoryStore and raises otherwise. Production stores (Postgres, OpenFGA) will override; kept minimal to avoid designing an API we don't need yet.

…store

Closes #4, #5, #6.

core/store.py
  TupleStore Protocol + InMemoryStore. Deliberately different data
  shape from oracle.py (adjacency dict vs. linear iteration) so the two
  cannot share bugs. `document_barrier_tags` on the store gives check()
  everything it needs for deny evaluation without importing Document.

core/rebac.py
  The real engine. Depth-limited (MAX_DEPTH=8), cycle-safe DFS via an
  in_progress stack set. Barrier check runs first and short-circuits.
  Reason path is the chain of ReasonSteps produced — that is what makes
  W3's audit log usable. Different algorithm from oracle.py (DFS vs
  BFS): a bug common to both would have to originate in algebra.py.

tests/test_rebac.py
  18 handwritten fixtures targeting DFS-specific failure modes: longer
  path chosen when shorter is expired, diamond backtracking, self-loop,
  mutual cycle, off-by-one at MAX_DEPTH, barriers at leaf and deep
  grants, reason-path chain integrity.

evals/differential.py
  Wired to rebac. xfail markers removed. `WARDEN_HYP_MAX` env var
  overrides the example count so CI can run the 5000-example acceptance
  gate. Green at 5000 examples locally (~1m43s).

evals/generators.py
  Fix: enforce unique doc IDs in generated graphs. Document.id is a PK
  in the real schema; a frozenset with duplicate ids but distinct
  barrier_tags would produce nondeterministic iteration and spurious
  differential failures. Caught by the harness itself on first run —
  the intended failure mode.

sql/001_init.sql
  Initial schema: tuples (with expires_at, subject_rel, fwd/rev
  indexes) and barriers. CHECK constraint prevents self-side barriers.

core/postgres_store.py
  PostgresStore + CRUD (write_tuple, delete_tuple, list_tuples,
  write_barrier, load_graph, apply_migrations). group_memberships uses
  a recursive CTE — one round trip regardless of nesting depth, cycle-
  safe via UNION dedup. Idempotent writes via ON CONFLICT DO NOTHING.

tests/test_postgres_store.py
  7 integration tests. Skipped unless WARDEN_TEST_DB_URL is set, so
  contributors without Docker can still run make test. Covers CRUD,
  idempotency, expires_at round-trip, and end-to-end rebac.check()
  against a Postgres-backed store (including 5-hop nesting and
  barrier deny).

docker-compose.yml
  Postgres 16 alpine. No volume mount for sql/ (portability with
  iCloud/WSL bind mounts); migrations are applied from Python via
  apply_migrations().

.github/workflows/ci.yml
  Three jobs: oracle+rebac+generators (fast), differential at 5000
  examples (W1 gate), and postgres-integration with a service container.

Makefile
  Split rebac, differential, differential-gate (5000), postgres-up,
  postgres-down, postgres-integration targets.

Test state: 41 unit tests + 3 differential properties (5000 examples
each, in CI) + 7 Postgres integration tests. All green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@geminimir
geminimir merged commit 8bb7529 into main Jul 16, 2026
3 checks passed
@geminimir
geminimir deleted the w1/rebac-core branch July 16, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

W1.1 — Postgres schema: tuples + barriers

1 participant