"Which authorization engine should I use" always gets answered with a table. Cedar is analyzable, Rego is expressive, Zanzibar is relationship-based. I could never feel the difference from any of it.
So I gave four of them the same rules and ran every possible request through all four at once.
- Live demo: https://0-draft.github.io/authz-playground/
- Repo: 0-draft/authz-playground
One question, asked of every engine at every hour of the day. A raised line is allow. Red is where they disagree.
Where ReBAC stops
The page adds one rule at a time. The owner can edit. Folder admins too. Then: only during business hours.
That third one is where ReBAC stops keeping up. check(user, relation, object) has three arguments and none of them holds a clock, so it goes on allowing edits at 3am while the other three deny them. Eight of 72 requests split.
That gap is why OpenFGA later added Conditions. Their announcement names time of day as the example relationships handle badly.
The playground now has that fix as a fifth row: the same Zanzibar model with an OpenFGA condition. The grant stays relational, and one more relation gates it:
define within_hours: [user:* with business_hours]
define editor: (owner or admin from parent) and within_hours
It agrees with the other three on every request. It is still not free. A condition can only hang off a tuple, and business hours are about the request rather than any relationship, so every document needs a placeholder tuple whose only job is to carry the clock. The page rates it possible but forced, not native.
Add a document and Casbin needs its policy table regenerated, while ReBAC just gains a tuple and the model never moves.
What broke
The projections are where I could lie without noticing, so they get checked twice: every engine runs every request and any disagreement is reported, and a separate implementation written from the requirement text alone checks they are not all wrong together.
Both earned their keep.
I gave the ReBAC model viewer: owner or editor, because obviously an editor can read too. The requirements never said that. Disagreements doubled and the comparison was quietly dishonest until the harness flagged it.
Worse, the request enumeration sampled 03:00, 10:00, 14:00 and 22:00, so neither edge of the business-hours window was ever evaluated. I moved one engine's window to 08:00-19:00 to see what would happen. All fourteen tests passed. The suite that existed to protect the comparison could not see the one boundary the whole thing rests on.
The fix was to sample both edges of the window and the hour either side of them, 08, 09, 17 and 18, plus a test that fails if any of those ever drops out of the enumeration. The 08:00-19:00 window now fails at once.
How it runs
No backend. Cedar through its official wasm bindings, Casbin's TypeScript build, and about a hundred lines reimplementing the Zanzibar check algorithm. Rego is OPA itself compiled to js/wasm, so policies are compiled and evaluated live: 8 MB gzipped, loaded lazily, roughly 2 ms per evaluation after the first.
If you try that last one, restrict the builtins. I shipped the default set, which left http.send reachable from pasted Rego. It fired a request from the visitor's browser and deadlocked the Go runtime for the rest of the session.
Have a go at stage 3 and drag across the chart: 0-draft.github.io/authz-playground


Top comments (1)
The business hours example is the one I keep coming back to. A relation check can say who is connected to a document, but it has no place to put the clock.
In practice we split the question in two. Relationships handle ownership and folder inheritance, then a condition set handles time, amount or clearance at check time. I work at Permit, and that split is how we model it there, RBAC or ReBAC for the grant itself and ABAC conditions on top.
The boundary test matters more than the engine choice. If the suite never evaluates 08:00 and 19:00 exactly, any of the four engines can pass while the rule is wrong.