feat(w1): ReBAC core — check() with reason paths, barriers, Postgres store#21
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.pyis judged againstcore/oracle.pyon 5,000 randomly generated authorization graphs per CI run, across three properties (safety, fidelity, pointwise).What ships
core/store.py—TupleStoreProtocol +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,CHECKagainst self-side barriers) and a full store implementation.group_membershipsuses 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-endrebac.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
Documentobjects sharing the sameid, which is illegal in the real schema (idis a PK). Oracle and engine happened to pick different rows via nondeterministicfrozensetiteration, producing a spurious disagreement. Fixed inevals/generators.pyby 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 withoutWARDEN_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.Invariant checklist
make oraclegreen.make testgreen.xfailmarker removed, tests are truly green.core/oracle.pyandcore/rebac.py. Different algorithms (BFS vs. DFS), different data structures (linear iteration vs. adjacency dict), different barrier evaluation code paths.Security-sensitive?
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.
Notes for reviewers
_first_blocking_barrierreturningNonefor non-doc objects is intentional (see comment). Barrier tags currently only attach to documents; container-level inheritance is a W2 concern via label materialization._doc_tagsside channel onPostgresStoreis a W1-only shim — in W2 the store will read barrier tags from the documents table. Documented in place.expand()usesstore._outgoingdirectly forInMemoryStoreand raises otherwise. Production stores (Postgres, OpenFGA) will override; kept minimal to avoid designing an API we don't need yet.