Drop-in REBAC engine for Django, SpiceDB-compatible, with a pure-Django LocalBackend.
Project description
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, and system checks.SpiceDBBackendand adapter modules continue to evolve. 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 an 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).
Two backends are interchangeable behind one Python API:
LocalBackend— pure Django, evaluates permissions via recursive CTEs against a singleRelationshiptable. Zero external infrastructure. Suitable up to ~10M relationships and depth ≤ 8.SpiceDBBackend— wraps the officialauthzedPython client. Connects to a SpiceDB cluster. Drop-in swap whenLocalBackendis no longer enough — same Python API, no code changes, justREBAC_BACKEND = "spicedb"in settings.
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 MCP servers, Celery tasks, GraphQL resolvers, and management commands — 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 plugin itself only ships auth/user and auth/group schema (mapped onto django.contrib.auth); agents/agent, agents/grant, auth/apikey, and other subject types live in your own apps.
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 Relationship + Schema* tables
python manage.py rebac sync # loads permissions.zed into Schema* tables
# 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, MCP tools, and GraphQL resolvers — 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, MCP tools, AI agents (Grant pattern), Celery tasks, 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: recursive CTE on a single Django table. Same API surface as SpiceDBBackend. |
| AI-agent authorization | Cedar (no graph traversal); Casbin (in-memory post-filter); Polar/Oso (deprecated 2023) | Native Authzed Grant pattern: an agent acting on behalf of a user receives the structural intersection of the user's grants and the agent's declared capabilities — enforced by the schema graph, not by app-layer ANDs. |
| Permission scoping outside HTTP | Manual if user.has_perm(...) everywhere |
Model.objects.with_actor(actor) works in MCP servers, Celery tasks, cron, management commands, plain Python — anywhere. 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 subject built-in.
auth/anonymous:*is shipped alongsideauth/userandauth/group. The default resolver returns it for unauthenticated requests; schemas reference it via the bareanonymouskeyword or theauth/anonymous:*wildcard. 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. Default flips to"registry"in v0.5. 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), runtime-editableincludes/effective_member. 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(default"angee/role:admin"); set toNoneto disable. 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). See docs/ARCHITECTURE.md § Per-request evaluator + Zookie freshness. - Write-then-read freshness via Zookie ContextVar. Every write returns a
Zookie; subsequent reads in the same scope auto-upgrade toConsistency.AT_LEAST_AS_FRESH(zookie)so the SpiceDB dispatcher-cache staleness window closes by default. Cross-request transport (SPA / JWT) opt-in viaREBAC_ZOOKIE_TRANSPORT = "header" | "session". LocalBackend usesRelationship.written_at_xidas the freshness witness; the API is backend-agnostic. - Celery actor propagation built in.
before_task_publishinjects the actor into task headers;task_prerunrestores it on the worker. Inside@shared_task, scoping happens transparently. - MCP-aware.
@rebac_mcp_tooldecorator wraps FastMCP / official-SDK tool functions; resolves the actor fromctx.request_context.meta; checks before the tool body runs. - 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.11 / 3.12 / 3.13 / 3.14 | 4.2 LTS | ✅ planned |
| 3.11 / 3.12 / 3.13 / 3.14 | 5.2 LTS | ✅ planned |
| 3.13 / 3.14 | 6.0 | ✅ Python 3.14 + Django 6.0 covered by CI |
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+ (supported), SQLite (test/dev only — recursive CTE performance is not production-grade). The Relationship table ships with all required indexes in 0001_initial.py.
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_mcp_tool │
│ │ │
│ ┌───────────────▼──────────────────┐ │
│ │ rebac.backends.Backend (ABC) │ │
│ │ check_access has_access │ │
│ │ accessible lookup_subjects │ │
│ └───────────────┬──────────────────┘ │
│ ┌─────────────┴────────────┐ │
│ │ │ │
│ ┌──────────▼──────────┐ ┌───────────▼───────────┐ │
│ │ LocalBackend │ │ SpiceDBBackend │ │
│ │ recursive CTE + │ │ authzed.api.v1.Client │ │
│ │ cel-python caveats │ │ → gRPC to spicedb │ │
│ └─────────────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Both backends are line-for-line API-compatible. The migration path is well-defined: prove your schema in LocalBackend first, then flip REBAC_BACKEND = "spicedb" and point at a SpiceDB cluster when scale or graph depth demands it. Persisted consistency tokens (Zookies) are not portable across the swap; this is the only operational consideration documented prominently in ARCHITECTURE.md § Migration safety.
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+ —
SpiceDBBackendhardening, broader CI matrix, and MCP / GraphQL adapters. - 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 (planned, matches 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_zed_rebac-0.7.0.tar.gz.
File metadata
- Download URL: django_zed_rebac-0.7.0.tar.gz
- Upload date:
- Size: 190.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
924d45bc4dc3b5255308c6dcd99db886f847ad6bdd9ca59001f0833e32c1e22d
|
|
| MD5 |
8a38d68fed7da8f1b66128be0053f4e1
|
|
| BLAKE2b-256 |
20eda8d55e11edc2eb1f8cf359ce52796cf6a567190a4d171f9da00896b8f6ae
|
File details
Details for the file django_zed_rebac-0.7.0-py3-none-any.whl.
File metadata
- Download URL: django_zed_rebac-0.7.0-py3-none-any.whl
- Upload date:
- Size: 150.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f715f320b69322995c94ea2a345b2ab1fc0eb70d6846c83a855277255c362fd8
|
|
| MD5 |
27a545ff76bcd3474072d594417ef62a
|
|
| BLAKE2b-256 |
25dd68322e6fab3efe57a590dc1da3fa5b8520d93cd45928de9b3694ab71d677
|