django-zed-rebac
SpiceDB-compatible REBAC for any Django project. Drop in, declare your permission schema in a per-app
permissions.zedfile, and every queryset, save, and method call is gated against the effective user.
Status: alpha. The package is published on PyPI and the core local REBAC path is usable:
LocalBackend,RebacMixin/manager, schema parser,rebac sync, caveats, expirations, schema overrides, audit events, middleware, system checks, DRF helpers, the Strawberry GraphQL adapter, a REBAC-safe Strawberry-Django optimizer, field-backed structural relations, and the FastMCPrebac_mcp_tooladapter.SpiceDBBackendis roadmap work. Track the milestones at docs/ARCHITECTURE.md § Roadmap.
What it is
django-zed-rebac ports SpiceDB's relation-based access control model into Django. You author your permission schema as a SpiceDB-native .zed file alongside your app; the plugin loads it into DB tables on install with noupdate=True semantics that preserve admin edits, and admins tweak overrides through the admin UI (or your own GraphQL layer).
The backend interface is designed around one Python API:
LocalBackend— pure Django. A permission is compiled to a query over your own tables and the relationship table and evaluated when it is read: field-, attribute- and const-backed relations come from the model columns, stored relations from tuples. The library stores no row per application row, there is no build step, and a write is visible to the next read. Recursive permissions are unrolled toREBAC_DEPTH_LIMITlevels. Caveats are decided in Python and bound into the statement. See the design and its limits.SpiceDBBackend— planned adapter for the officialauthzedPython client. The class exists today as a clear stub, butREBAC_BACKEND = "spicedb"is not a supported runtime path yet.
Add the mixin to your model and Post.objects.all() returns only what the user can read. Add Model.objects.with_actor(actor) for explicit actor scoping in Celery tasks, GraphQL resolvers, management commands, and other non-HTTP entrypoints — actor can be a Django User, a registered Agent, an agents/grant (agent-acting-on-behalf-of-user, shipped by your agents app), or anything @rebac_subject-registered. Typed shorthands as_user(user) and as_agent(agent, on_behalf_of=user) cover the common cases. The default Django actor labels are auth/user and auth/group; include their definitions in your application schema (automatic base-schema emission is planned). agents/agent, agents/grant, auth/apikey, and other subject types live in your own apps.
The agent shorthands resolve identities only. Applications own the delegation schema and grant relationships; choosing a grant subject does not create those relationships or inherit the requester's permissions.
Quickstart
# settings.py
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
# ...
"rebac",
"blog",
]
AUTHENTICATION_BACKENDS = [
"rebac.backends.RebacBackend",
"django.contrib.auth.backends.ModelBackend",
]
REBAC_BACKEND = "local"
// blog/permissions.zed
// @rebac_package: blog
// @rebac_package_version: 0.1.0
// @rebac_schema_revision: 1
definition blog/post {
relation owner: auth/user
relation viewer: auth/user | auth/group#member | auth/user:*
permission read = owner + viewer
permission write = owner
permission delete = owner
}
# blog/apps.py
class BlogConfig(AppConfig):
name = "blog"
rebac_schema = "permissions.zed" # relative to the app's package dir
# blog/models.py
from django.db import models
from rebac import RebacMixin
class Post(RebacMixin, models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
class Meta:
rebac_resource_type = "blog/post"
python manage.py migrate # creates the library's tables
python manage.py rebac sync # loads permissions.zed into the schema tables
Until the first sync publishes a policy, permission reads are closed. Nothing
else has to be built, and fixture loads or bulk writes need no command
afterwards. Write aliases for the models a permission reads and for
relationships must match; separate read replicas are allowed. After a database
flush, run rebac sync again before serving. See the
upgrade notes.
# blog/views.py
def post_detail(request, pk):
post = get_object_or_404(Post.objects.with_actor(request.user), pk=pk)
return render(request, "post.html", {"post": post})
That's the end-to-end flow. The same Post.objects.with_actor(...) pattern works in DRF viewsets, Celery tasks, GraphQL resolvers, management commands, and MCP tools — the actor can be a Django User, an Agent, an agents/grant, or any registered subject. Typed shorthands as_user(request.user) and as_agent(agent, on_behalf_of=request.user) cover the common cases.
Documentation
| Doc | When to read it |
|---|---|
| docs/ARCHITECTURE.md | You're integrating, contributing, or evaluating fit. Architecture, public API, the three storage tiers, settings, surface integrations, determinism, testing, roadmap. |
| docs/ZED.md | You're writing permission schemas. How to define permissions for users, groups, AI agents (Grant pattern), Celery tasks, MCP tools, hierarchical resources, time-bound access, arbitrary Python entities. Patterns library and anti-patterns. |
Why use this
| Problem | Existing options | What django-zed-rebac does |
|---|---|---|
| Per-object permissions in Django | django-guardian (per-object ACL via GenericFK; no JOIN propagation; no graph traversal) |
True REBAC graph; SpiceDB-compatible; manager-level queryset scoping; cross-relation propagation. |
| Run SpiceDB-style permissions locally without infrastructure | None — SpiceDB itself is a Go binary that needs Postgres + a sidecar | LocalBackend: pure Django; permissions are compiled to queries over relationships and declared backings. Same API surface the planned SpiceDBBackend will use. |
| AI-agent authorization | Cedar (no graph traversal); Casbin (in-memory post-filter); Polar/Oso (deprecated 2023) | Consumer-defined grant objects and permission expressions can combine delegation and capability conditions. The engine evaluates those explicit relationships; resolving an agent or grant subject does not confer the user's permissions. |
| Permission scoping outside HTTP | Manual if user.has_perm(...) everywhere |
Model.objects.with_actor(actor) works in Celery tasks, cron, management commands, plain Python, and MCP servers. The actor is generic: Django User, Agent, agents/grant, auth/apikey, or any registered subject. |
| Strict-by-default (no silent leaks) | django-guardian returns all rows when nothing scopes; easy to forget |
Querysets without an actor raise MissingActorError rather than returning everything. Bypass requires an explicit reason; block-scoped sudo() is logged. |
| Admin-editable policy with safe upgrades | django-guardian per-object ACL only; no rule overrides |
Tier-2 SchemaOverride model: tighten / loosen / disable / extend a package-shipped baseline at runtime. noupdate=True semantics preserve admin edits across upgrades, mirroring Odoo's ir.model.data. |
Highlights
- Three storage tiers, three editors. Tier 1 (structural, code-shipped
.zed) → Tier 2 (override, admin-editable) → Tier 3 (relationships, runtime data). Clear ownership rule per tier — see docs/ARCHITECTURE.md § Conceptual model. - Anonymous actor resolution. The default resolver returns
auth/anonymous:*for unauthenticated requests. The bareanonymouspermission term is built in; applications declare any subject definitions their relations reference, includingauth/anonymous. Automatic base-schema emission is not implemented. Subject type configurable viaREBAC_ANONYMOUS_TYPE. See docs/ARCHITECTURE.md § Anonymous subject. - Two LocalBackend storage shapes.
REBAC_LOCAL_BACKEND_STORAGE = "denormalized"(default) storesresource_type/resource_id/subject_type/subject_idas wide string columns — the historical shape.REBAC_LOCAL_BACKEND_STORAGE = "registry"collapses them into two integer FKs into a sharedRebacResourcetable, yielding ~5-10x index-density gain on the hot path plus FK-CASCADE cleanup when the underlying Django row is deleted. Migration between the two is a one-shot viapython manage.py rebac migrate-storage --to registry. See docs/ARCHITECTURE.md § Storage modes. - Predefined-role helpers (
rebac.roles). GCP-style role-as-resource pattern packaged asgrant/revoke/roles_of/members_ofplusimply/unimply/implies_of/implied_by_offor runtime-editable hierarchy. Roles live as objects in<namespace>/roleresource types; grants areRelationshiprows. Three role-hierarchy recipes — type-union inclusion (relation member: ... | parent_role:X#member), per-resource permission composition (permission read = viewer + editor + admin), and a directincludes: rolerelation withincludes->effective_membertraversal. See docs/ARCHITECTURE.md §rebac.roles. - Universal-admin lint (
rebac.W004). Optional system check that warns when a<namespace>/roledefinition is missing the universal-admin role's#membersubject in itsmembertype union. Configurable viaREBAC_UNIVERSAL_ADMIN_ROLE(defaultNone); set an application-owned role reference to enable it. Catches the "I forgot to thread the admin override through" footgun at startup. - Unified check API.
check_access(op)/has_access(op)/accessible(op)— one entrypoint family, borrowed from Odoo 18's PR #179148 unification. No model-level vs record-level split at the call site. - One mixin gates everything. Add
RebacMixinto a model, declareMeta.rebac_resource_type, and queries / writes / method calls / FK reverse accessors are all permission-aware. No per-viewset wiring. - Field-level read gates. Schema permissions named
read__<field>redact or omit denied fields at queryset materialisation time whenREBAC_FIELD_READ_MODE = "redact" | "omit"(default"allow"). Redacted fields are excluded from later full saves so a display-timeNonecannot overwrite the stored value. with_actor(actor)≠sudo(reason=...). Distinct verbs for distinct intents.with_actorre-evaluates checks as that subject (user, agent, grant, apikey, …);sudobypasses them with mandatoryreasonand audit-log entry. Originating uid preserved through bypass for audit. Sudo does NOT propagate through relationship traversal — every related read re-resolves against the carrying scope.- Strict by default. A queryset without an actor scope raises rather than leaking. Bypass requires an explicit reason; block-scoped
sudo()writes a structured audit event. - Drop-in DRF integration.
permission_classes = [RebacPermission]+filter_backends = [RebacFilterBackend]. Per-action permission map; customisable. - GraphQL + WebSocket-aware. Per-request
PermissionEvaluatorLRU-cachescheck_access/accessiblecalls — a single GraphQL query that fans out across 50 nested resolvers makes 1 backend call per(actor, action, resource), not 50. The Strawberry adapter (pip install django-zed-rebac[strawberry]) shipsRebacExtension(per-operation scope, per-emission for subscriptions) andRebacChannelsConsumerMixin(actor resolved at WS handshake; per-emission cache reset means revoked grants take effect on the next subscription tick). Strawberry-Django users can installdjango-zed-rebac[strawberry-django]and useRebacDjangoOptimizerExtensionfor permission-awareselect_related/prefetch_relatedoptimization. See docs/ARCHITECTURE.md § Per-request evaluator + Zookie freshness. - Write-then-read freshness via Zookie ContextVar. Every write returns a
Zookie; subsequent LocalBackend reads in the same scope auto-upgrade toConsistency.AT_LEAST_AS_FRESH(zookie). Cross-request transport (SPA / JWT) is opt-in viaREBAC_ZOOKIE_TRANSPORT = "header" | "session". LocalBackend usesRelationship.written_at_xidas the freshness witness; the same public API is reserved for the planned SpiceDB adapter. - Celery tasks use explicit actor scope. Pass the authenticated actor reference through your trusted task producer, then use
actor_context()or.with_actor()inside the worker. Automatic Celery propagation is planned; norebac.celerymodule is shipped. - MCP integration shipped. Model MCP tools as schema resources and gate them with
from rebac.mcp import rebac_mcp_tool— it resolves the actor from the request context (ctx.request_context.meta["actor_subject"]by default), checks the permission, then runs the tool body. Sync and async tools both supported. See proposal 0004. - Three-state checks. Like SpiceDB,
check_access()returnsHAS_PERMISSION,NO_PERMISSION, orCONDITIONAL_PERMISSION(missing=[...])— the latter lists which caveat fields the caller must supply for a definitive answer. - Typed package. Ships
py.typedand keeps the public API annotated so downstream projects and IDEs can reason about the manager/queryset surface. noupdate=Trueupgrade safety. Admin schema edits are preserved across package upgrades. Destructive overwrite is an explicit--force-overwriteflag, never an implicit side effect of install vs upgrade. Engineers Odoo's-ifootgun out.- Deterministic build.
python manage.py rebac sync --checkis a CI gate that returns non-zero on schema drift, mirroringmigrate --check.
Compatibility
| Python | Django | Status |
|---|---|---|
| 3.14 | 6.0 | ✅ supported and covered by CI |
The package constrains Django to 6.0.x while newer feature releases await verification of its ORM integration.
Versioning follows SemVer while the project is below 1.0: minor releases may add public API and tighten alpha contracts; patch releases are reserved for compatible fixes.
Database support: PostgreSQL 13+ (production target), MySQL 8+ (opt-in vendor suite), SQLite (test/dev only). CI runs SQLite and PostgreSQL; the MySQL suite is a release gate. The Relationship indexes ship in 0001_initial.py. After upgrading, run migrate; no other command is needed. Behaviour on PostgreSQL tables of tens of millions of rows has not been verified yet. See upgrade notes for the removed commands, checks and settings and for the recursion bound.
Comparison
| Library | Per-object | Graph traversal | SpiceDB-compatible | AI-agent pattern | Maintained |
|---|---|---|---|---|---|
django-zed-rebac |
✅ | ✅ | ✅ | ✅ Grant pattern | (in development) |
django-guardian |
✅ (ACL via GenericFK) | ❌ | ❌ | ❌ | ✅ |
django-rules |
❌ (predicate engine) | ❌ | ❌ | ❌ | ✅ |
django-spicedb |
proxy only | ✅ (via SpiceDB) | ✅ | ❌ | ❌ (early/inactive) |
casbin-django-orm-adapter |
✅ (ACL, in-memory) | partial | ❌ | ❌ | ✅ |
django-oso |
✅ | ✅ | ❌ | ❌ | ❌ (deprecated 2023) |
django-rls (PostgreSQL RLS) |
✅ | DB-level only | ❌ | ❌ | ✅ |
zanzipy |
✅ | ✅ | partial | ❌ | ❌ (early/single-author) |
django-zed-rebac is the first Django package targeting full SpiceDB schema-language compatibility AND a working in-process backend AND a first-class AI-agent pattern AND admin-editable policy with safe upgrades — none of the others combine all four.
Backends in detail
┌─ Your application ──────────────────────────────────────────────┐
│ RebacMixin / RebacPermission / @rebac_resource │
│ │ │
│ ┌───────────────▼──────────────────┐ │
│ │ rebac.backends.Backend (ABC) │ │
│ │ check_access has_access │ │
│ │ accessible lookup_subjects │ │
│ └───────────────┬──────────────────┘ │
│ ┌─────────────┴────────────┐ │
│ │ │ │
│ ┌──────────▼──────────┐ ┌───────────▼───────────┐ │
│ │ LocalBackend │ │ SpiceDBBackend │ │
│ │ compiled queries + │ │ planned authzed │ │
│ │ cel-python caveats │ │ adapter │ │
│ └─────────────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
LocalBackend is the supported runtime backend today. SpiceDBBackend is on the roadmap behind the same Backend interface; persisted consistency tokens (Zookies) are intentionally backend-tagged and will not be portable across that future swap.
What django-zed-rebac is NOT
- Not a User model. Use
django.contrib.auth.models.Useror any swappableAUTH_USER_MODEL. - Not an authentication system. Use
django-allauth,dj-rest-auth,simple-jwt, or your own. - Not a session manager. Django's session middleware is fine.
- Not a multi-tenant database router. Use
django-tenantsordjango-organizations.django-zed-rebacis orthogonal — it works inside whatever tenant scope the project provides. (For soft tenancy in a single DB, seeREBAC_TYPE_PREFIXin ARCHITECTURE.md.) - Not a GraphQL admin layer. A future
django-zed-rebac-adminpackage may add one; v1 ships a Django admin form forSchemaOverride. Higher-level frameworks may layer their own admin surfaces on top. - Not a policy DSL like Polar or Cedar. The schema language is SpiceDB's
.zed, REBAC-first. ABAC fragments are expressed via caveats.
Status & roadmap
This is an alpha package. The architecture is settled (see docs/ARCHITECTURE.md) and releases are published to PyPI. Milestones:
- 0.1 —
LocalBackendMVP, schema parser + sync command,RebacMixin, system checks, sync/check commands. - 0.2 — Alpha hardening: schema-level built-in actors, action-scoped queryset reads, split
sudo()/system_context(), and efficient schema cache invalidation. - 0.3-0.9 — shipped middleware, registry storage mode, evaluator/Zookie scopes, Strawberry adapter, field-level read gates, REBAC-safe relation loading, Strawberry-Django optimizer, field-backed structural relations, and LocalBackend hardening.
- 0.11 — FastMCP
rebac_mcp_tooladapter (proposal 0004). - 0.23–0.24 — full subject expansion, reference and differential test suites, PostgreSQL CI, write gates for backed edges.
- Unreleased — permissions compiled to queries over the application's own tables (proposal 0015): no derived tables, no build step. The trial on a large PostgreSQL database is pending.
- Next —
SpiceDBBackendadapter. - 1.0 — Stable release with full docs and CI matrix green.
Track the full plan in docs/ARCHITECTURE.md § Roadmap.
Contributing
See CONTRIBUTING.md for local setup and checks. Design feedback is welcome via GitHub issues — schema-language proposals, missing scenarios, integration-surface concerns, anything in ARCHITECTURE.md § Open questions you'd push back on.
License
Apache-2.0, matching authzed-py, cel-python, and spicedb itself.
Acknowledgments
django-zed-rebac is a faithful Django port of the model described in Google's Zanzibar paper and as implemented by SpiceDB. The schema language and API surface mirror SpiceDB's conventions exactly. The Grant pattern for AI-agent authorization is from Authzed's Secure AI Agents tutorial. The unified check API (check_access / has_access / accessible) is borrowed from Odoo 18 PR #179148. The noupdate=True upgrade-safety semantic is borrowed from Odoo's ir.model.data. Caveat evaluation in LocalBackend uses cel-python.
Metadata
Release files for django-zed-rebac 0.28.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| django_zed_rebac-0.28.0.tar.gz | 602.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_zed_rebac-0.28.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 918.2 kB
Release files / django_zed_rebac-0.28.0.tar.gz
| Download URL | django_zed_rebac-0.28.0.tar.gz |
|---|---|
| Size | 602.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c076bc84fcec302ee4fed96568dc7f683b5510da7f43f47ac769b0c34da4139f
|
|
BLAKE2b-256 checksum How to use checksums |
9f78da79147d0d32a0a1e69c3d574de82410932abc64148d2ec2b50ce74edd5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / django_zed_rebac-0.28.0-py3-none-any.whl
| Download URL | django_zed_rebac-0.28.0-py3-none-any.whl |
|---|---|
| Size | 315.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a5fcd2b1a5fb527465dd4d7a1ae140accb95384b2717c59ad892d461c4acae8b
|
|
BLAKE2b-256 checksum How to use checksums |
f5b97fde3e6ab1f6d3c72d51950802db89c9454ff177755683ca4fadac0f805a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for django-zed-rebac 0.28.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.