Skip to main content

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.zed file, 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, Celery actor propagation, the Strawberry GraphQL adapter, a REBAC-safe Strawberry-Django optimizer, field-backed structural relations, and the FastMCP rebac_mcp_tool adapter. SpiceDBBackend is 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, evaluates permissions against local relationship rows. Zero external infrastructure. Suitable up to moderate graph sizes and depth <= 8.
  • SpiceDBBackend — planned adapter for the official authzed Python client. The class exists today as a clear stub, but REBAC_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 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, 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 graph evaluation over relationship rows. Same API surface the planned SpiceDBBackend will use.
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 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 subject built-in. auth/anonymous:* is shipped alongside auth/user and auth/group. The default resolver returns it for unauthenticated requests; schemas reference it via the bare anonymous keyword or the auth/anonymous:* wildcard. Subject type configurable via REBAC_ANONYMOUS_TYPE. See docs/ARCHITECTURE.md § Anonymous subject.
  • Two LocalBackend storage shapes. REBAC_LOCAL_BACKEND_STORAGE = "denormalized" (default) stores resource_type / resource_id / subject_type / subject_id as wide string columns — the historical shape. REBAC_LOCAL_BACKEND_STORAGE = "registry" collapses them into two integer FKs into a shared RebacResource table, 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 via python 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 as grant / revoke / roles_of / members_of plus imply / unimply / implies_of / implied_by_of for runtime-editable hierarchy. Roles live as objects in <namespace>/role resource types; grants are Relationship rows. Three role-hierarchy recipes — type-union inclusion (relation member: ... | parent_role:X#member), per-resource permission composition (permission read = viewer + editor + admin), runtime-editable includes/effective_member. See docs/ARCHITECTURE.md § rebac.roles.
  • Universal-admin lint (rebac.W004). Optional system check that warns when a <namespace>/role definition is missing the universal-admin role's #member subject in its member type union. Configurable via REBAC_UNIVERSAL_ADMIN_ROLE (default "angee/role:admin"); set to None to 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 RebacMixin to a model, declare Meta.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 when REBAC_FIELD_READ_MODE = "redact" | "omit" (default "allow"). Redacted fields are excluded from later full saves so a display-time None cannot overwrite the stored value.
  • with_actor(actor)sudo(reason=...). Distinct verbs for distinct intents. with_actor re-evaluates checks as that subject (user, agent, grant, apikey, …); sudo bypasses them with mandatory reason and 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 PermissionEvaluator LRU-caches check_access / accessible calls — 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]) ships RebacExtension (per-operation scope, per-emission for subscriptions) and RebacChannelsConsumerMixin (actor resolved at WS handshake; per-emission cache reset means revoked grants take effect on the next subscription tick). Strawberry-Django users can install django-zed-rebac[strawberry-django] and use RebacDjangoOptimizerExtension for permission-aware select_related / prefetch_related optimization. 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 to Consistency.AT_LEAST_AS_FRESH(zookie). Cross-request transport (SPA / JWT) is opt-in via REBAC_ZOOKIE_TRANSPORT = "header" | "session". LocalBackend uses Relationship.written_at_xid as the freshness witness; the same public API is reserved for the planned SpiceDB adapter.
  • Celery actor propagation built in. before_task_publish injects the actor into task headers; task_prerun restores it on the worker. Inside @shared_task, scoping happens transparently.
  • 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() returns HAS_PERMISSION, NO_PERMISSION, or CONDITIONAL_PERMISSION(missing=[...]) — the latter lists which caveat fields the caller must supply for a definitive answer.
  • Typed package. Ships py.typed and keeps the public API annotated so downstream projects and IDEs can reason about the manager/queryset surface.
  • noupdate=True upgrade safety. Admin schema edits are preserved across package upgrades. Destructive overwrite is an explicit --force-overwrite flag, never an implicit side effect of install vs upgrade. Engineers Odoo's -i footgun out.
  • Deterministic build. python manage.py rebac sync --check is a CI gate that returns non-zero on schema drift, mirroring migrate --check.

Compatibility

Python Django Status
3.14 6.0 ✅ supported and 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 — local graph-walk 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.backends.Backend (ABC)   │                  │
│            │   check_access  has_access        │                  │
│            │   accessible    lookup_subjects   │                  │
│            └───────────────┬──────────────────┘                  │
│              ┌─────────────┴────────────┐                        │
│              │                          │                        │
│   ┌──────────▼──────────┐   ┌───────────▼───────────┐           │
│   │  LocalBackend       │   │  SpiceDBBackend        │           │
│   │  local graph walk + │   │  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.User or any swappable AUTH_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-tenants or django-organizations. django-zed-rebac is orthogonal — it works inside whatever tenant scope the project provides. (For soft tenancy in a single DB, see REBAC_TYPE_PREFIX in ARCHITECTURE.md.)
  • Not a GraphQL admin layer. A future django-zed-rebac-admin package may add one; v1 ships a Django admin form for SchemaOverride. 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.1LocalBackend MVP, 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, Celery propagation, 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_tool adapter (proposal 0004).
  • NextSpiceDBBackend adapter.
  • 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_zed_rebac-0.13.0.tar.gz (230.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_zed_rebac-0.13.0-py3-none-any.whl (177.5 kB view details)

Uploaded Python 3

File details

Details for the file django_zed_rebac-0.13.0.tar.gz.

File metadata

  • Download URL: django_zed_rebac-0.13.0.tar.gz
  • Upload date:
  • Size: 230.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_zed_rebac-0.13.0.tar.gz
Algorithm Hash digest
SHA256 8c0cc887ffd165fb1a59137b61c91f3188c2761a03c4cde7f578944ea6d26fe9
MD5 d628b621fa56e86d47d7bdc132e133e7
BLAKE2b-256 d48bdfa410f14da74dfefd03833784eaf8464c6df85c97b77cd5eee8a823ac4b

See more details on using hashes here.

File details

Details for the file django_zed_rebac-0.13.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_zed_rebac-0.13.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c29558e323374aa28eb1f3822665a3a5700c0bf07ae4c98eaed9f977574a4b35
MD5 dcbd9be43765ba9fad703ea2234f277f
BLAKE2b-256 e829e2ded31b19068777d136dc5d82e3226aee2ee2ece2d2647670b3b80295b3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page