django-zed-rebac 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. django_zed_rebac-0.1.4/MANIFEST.in +5 -0
  2. django_zed_rebac-0.1.4/PKG-INFO +242 -0
  3. django_zed_rebac-0.1.4/README.md +207 -0
  4. django_zed_rebac-0.1.4/pyproject.toml +91 -0
  5. django_zed_rebac-0.1.4/setup.cfg +4 -0
  6. django_zed_rebac-0.1.4/src/django_zed_rebac.egg-info/SOURCES.txt +63 -0
  7. django_zed_rebac-0.1.4/src/rebac/__init__.py +140 -0
  8. django_zed_rebac-0.1.4/src/rebac/_id.py +42 -0
  9. django_zed_rebac-0.1.4/src/rebac/actors.py +341 -0
  10. django_zed_rebac-0.1.4/src/rebac/admin.py +238 -0
  11. django_zed_rebac-0.1.4/src/rebac/apps.py +20 -0
  12. django_zed_rebac-0.1.4/src/rebac/audit.py +85 -0
  13. django_zed_rebac-0.1.4/src/rebac/backends/__init__.py +43 -0
  14. django_zed_rebac-0.1.4/src/rebac/backends/auth.py +188 -0
  15. django_zed_rebac-0.1.4/src/rebac/backends/base.py +82 -0
  16. django_zed_rebac-0.1.4/src/rebac/backends/local.py +870 -0
  17. django_zed_rebac-0.1.4/src/rebac/backends/spicedb.py +88 -0
  18. django_zed_rebac-0.1.4/src/rebac/caveats.py +229 -0
  19. django_zed_rebac-0.1.4/src/rebac/checks.py +171 -0
  20. django_zed_rebac-0.1.4/src/rebac/codenames.py +35 -0
  21. django_zed_rebac-0.1.4/src/rebac/composition.py +371 -0
  22. django_zed_rebac-0.1.4/src/rebac/conf.py +95 -0
  23. django_zed_rebac-0.1.4/src/rebac/decorators.py +85 -0
  24. django_zed_rebac-0.1.4/src/rebac/drf.py +129 -0
  25. django_zed_rebac-0.1.4/src/rebac/errors.py +53 -0
  26. django_zed_rebac-0.1.4/src/rebac/management/__init__.py +0 -0
  27. django_zed_rebac-0.1.4/src/rebac/management/commands/__init__.py +0 -0
  28. django_zed_rebac-0.1.4/src/rebac/management/commands/rebac.py +455 -0
  29. django_zed_rebac-0.1.4/src/rebac/managers.py +345 -0
  30. django_zed_rebac-0.1.4/src/rebac/middleware.py +59 -0
  31. django_zed_rebac-0.1.4/src/rebac/migrations/0001_initial.py +289 -0
  32. django_zed_rebac-0.1.4/src/rebac/migrations/__init__.py +0 -0
  33. django_zed_rebac-0.1.4/src/rebac/mixins.py +350 -0
  34. django_zed_rebac-0.1.4/src/rebac/models/__init__.py +29 -0
  35. django_zed_rebac-0.1.4/src/rebac/models/audit.py +44 -0
  36. django_zed_rebac-0.1.4/src/rebac/models/overrides.py +45 -0
  37. django_zed_rebac-0.1.4/src/rebac/models/provenance.py +33 -0
  38. django_zed_rebac-0.1.4/src/rebac/models/relationship.py +64 -0
  39. django_zed_rebac-0.1.4/src/rebac/models/schema.py +61 -0
  40. django_zed_rebac-0.1.4/src/rebac/permissions_mixin.py +260 -0
  41. django_zed_rebac-0.1.4/src/rebac/py.typed +0 -0
  42. django_zed_rebac-0.1.4/src/rebac/relationships.py +96 -0
  43. django_zed_rebac-0.1.4/src/rebac/resources.py +114 -0
  44. django_zed_rebac-0.1.4/src/rebac/schema/__init__.py +39 -0
  45. django_zed_rebac-0.1.4/src/rebac/schema/ast.py +136 -0
  46. django_zed_rebac-0.1.4/src/rebac/schema/parser.py +509 -0
  47. django_zed_rebac-0.1.4/src/rebac/signals.py +354 -0
  48. django_zed_rebac-0.1.4/src/rebac/types.py +168 -0
  49. django_zed_rebac-0.1.4/tests/test_actors.py +84 -0
  50. django_zed_rebac-0.1.4/tests/test_audit.py +234 -0
  51. django_zed_rebac-0.1.4/tests/test_auth_backend.py +273 -0
  52. django_zed_rebac-0.1.4/tests/test_build_zed.py +200 -0
  53. django_zed_rebac-0.1.4/tests/test_caveats.py +391 -0
  54. django_zed_rebac-0.1.4/tests/test_checks_and_conf.py +126 -0
  55. django_zed_rebac-0.1.4/tests/test_codenames.py +31 -0
  56. django_zed_rebac-0.1.4/tests/test_field_gates.py +261 -0
  57. django_zed_rebac-0.1.4/tests/test_id_attr.py +262 -0
  58. django_zed_rebac-0.1.4/tests/test_local_backend.py +164 -0
  59. django_zed_rebac-0.1.4/tests/test_middleware.py +139 -0
  60. django_zed_rebac-0.1.4/tests/test_mixin.py +367 -0
  61. django_zed_rebac-0.1.4/tests/test_overrides.py +524 -0
  62. django_zed_rebac-0.1.4/tests/test_parser.py +166 -0
  63. django_zed_rebac-0.1.4/tests/test_permissions_mixin.py +218 -0
  64. django_zed_rebac-0.1.4/tests/test_pickle_posture.py +132 -0
  65. django_zed_rebac-0.1.4/tests/test_rebac_object_meta.py +200 -0
  66. django_zed_rebac-0.1.4/tests/test_types.py +53 -0
@@ -0,0 +1,5 @@
1
+ include README.md
2
+ include LICENSE*
3
+ recursive-include src/rebac *.typed
4
+ recursive-exclude * __pycache__ *.py[cod]
5
+ prune src/django_zed_rebac.egg-info
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-zed-rebac
3
+ Version: 0.1.4
4
+ Summary: Drop-in REBAC engine for Django, SpiceDB-compatible, with a pure-Django LocalBackend.
5
+ Author-email: Apexive <hi@apexive.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/apexive/django-zed-rebac
8
+ Project-URL: Documentation, https://github.com/apexive/django-zed-rebac/tree/main/docs
9
+ Project-URL: Issues, https://github.com/apexive/django-zed-rebac/issues
10
+ Keywords: django,rebac,spicedb,zanzibar,authorization,permissions
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: Django
13
+ Classifier: Framework :: Django :: 4.2
14
+ Classifier: Framework :: Django :: 5.2
15
+ Classifier: Framework :: Django :: 6.0
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Security
21
+ Requires-Python: >=3.14
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: django>=4.2
24
+ Provides-Extra: spicedb
25
+ Requires-Dist: authzed>=0.13; extra == "spicedb"
26
+ Provides-Extra: caveats
27
+ Requires-Dist: cel-python>=0.1.5; extra == "caveats"
28
+ Provides-Extra: drf
29
+ Requires-Dist: djangorestframework>=3.14; extra == "drf"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-django>=4.8; extra == "dev"
33
+ Requires-Dist: ruff>=0.5; extra == "dev"
34
+ Requires-Dist: mypy>=1.10; extra == "dev"
35
+
36
+ # django-zed-rebac
37
+
38
+ > 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.
39
+
40
+ ---
41
+
42
+ > **Status: pre-alpha.** The architecture is settled and reviewed; Tier-1 source has landed (`LocalBackend`, `RebacMixin`/manager, schema parser, `rebac sync` command, system checks). Caveat evaluation, `SpiceDBBackend`, and adapter modules are in flight. Nothing on PyPI yet. Track the milestones at [docs/ARCHITECTURE.md § Roadmap](./docs/ARCHITECTURE.md#roadmap).
43
+
44
+ ---
45
+
46
+ ## What it is
47
+
48
+ `django-zed-rebac` ports [SpiceDB](https://github.com/authzed/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).
49
+
50
+ Two backends are interchangeable behind one Python API:
51
+
52
+ - **`LocalBackend`** — pure Django, evaluates permissions via recursive CTEs against a single `Relationship` table. Zero external infrastructure. Suitable up to ~10M relationships and depth ≤ 8.
53
+ - **`SpiceDBBackend`** — wraps the official [`authzed`](https://pypi.org/project/authzed/) Python client. Connects to a SpiceDB cluster. Drop-in swap when `LocalBackend` is no longer enough — same Python API, no code changes, just `REBAC_BACKEND = "spicedb"` in settings.
54
+
55
+ 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.
56
+
57
+ ## Quickstart
58
+
59
+ ```python
60
+ # settings.py
61
+ INSTALLED_APPS = [
62
+ "django.contrib.auth",
63
+ "django.contrib.contenttypes",
64
+ # ...
65
+ "rebac",
66
+ "blog",
67
+ ]
68
+
69
+ AUTHENTICATION_BACKENDS = [
70
+ "rebac.backends.RebacBackend",
71
+ "django.contrib.auth.backends.ModelBackend",
72
+ ]
73
+
74
+ REBAC_BACKEND = "local"
75
+ ```
76
+
77
+ ```zed
78
+ // blog/permissions.zed
79
+ // @rebac_package: blog
80
+ // @rebac_package_version: 0.1.0
81
+ // @rebac_schema_revision: 1
82
+
83
+ definition blog/post {
84
+ relation owner: auth/user
85
+ relation viewer: auth/user | auth/group#member | auth/user:*
86
+
87
+ permission read = owner + viewer
88
+ permission write = owner
89
+ permission delete = owner
90
+ }
91
+ ```
92
+
93
+ ```python
94
+ # blog/apps.py
95
+ class BlogConfig(AppConfig):
96
+ name = "blog"
97
+ rebac_schema = "permissions.zed" # relative to the app's package dir
98
+
99
+ # blog/models.py
100
+ from django.db import models
101
+ from rebac import RebacMixin
102
+
103
+ class Post(RebacMixin, models.Model):
104
+ title = models.CharField(max_length=200)
105
+ body = models.TextField()
106
+ author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
107
+
108
+ class Meta:
109
+ rebac_resource_type = "blog/post"
110
+ ```
111
+
112
+ ```bash
113
+ python manage.py migrate # creates Relationship + Schema* tables
114
+ python manage.py rebac sync # loads permissions.zed into Schema* tables
115
+ ```
116
+
117
+ ```python
118
+ # blog/views.py
119
+ def post_detail(request, pk):
120
+ post = get_object_or_404(Post.objects.with_actor(request.user), pk=pk)
121
+ return render(request, "post.html", {"post": post})
122
+ ```
123
+
124
+ 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.
125
+
126
+ ## Documentation
127
+
128
+ | Doc | When to read it |
129
+ |---|---|
130
+ | **[docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)** | You're integrating, contributing, or evaluating fit. Architecture, public API, the three storage tiers, settings, surface integrations, determinism, testing, roadmap. |
131
+ | **[docs/ZED.md](./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. |
132
+
133
+ ## Why use this
134
+
135
+ | Problem | Existing options | What `django-zed-rebac` does |
136
+ |---|---|---|
137
+ | 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. |
138
+ | 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`. |
139
+ | 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. |
140
+ | 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. |
141
+ | 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 explicit `.sudo(reason=...)` and is logged. |
142
+ | 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`. |
143
+
144
+ ## Highlights
145
+
146
+ - **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](./docs/ARCHITECTURE.md#conceptual-model).
147
+ - **Unified check API.** `check_access(op)` / `has_access(op)` / `accessible(op)` — one entrypoint family, borrowed from [Odoo 18's PR #179148](https://github.com/odoo/odoo/pull/179148) unification. No model-level vs record-level split at the call site.
148
+ - **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.
149
+ - **`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.
150
+ - **Strict by default.** A queryset without an actor scope raises rather than leaking. Bypass requires explicit `.sudo(reason="cron.expire_drafts")` and writes a structured audit event.
151
+ - **Drop-in DRF integration.** `permission_classes = [RebacPermission]` + `filter_backends = [RebacFilterBackend]`. Per-action permission map; customisable.
152
+ - **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.
153
+ - **MCP-aware.** `@rebac_mcp_tool` decorator wraps FastMCP / official-SDK tool functions; resolves the actor from `ctx.request_context.meta`; checks before the tool body runs.
154
+ - **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.
155
+ - **Type-checked.** Ships `py.typed`. `RebacManager[M]` is `Generic[M]` — your IDE infers the right model class through the manager. `mypy --strict` and `pyright` both run on CI.
156
+ - **`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.
157
+ - **Deterministic build.** `python manage.py rebac sync --check` is a CI gate that returns non-zero on schema drift, mirroring `migrate --check`.
158
+
159
+ ## Compatibility
160
+
161
+ | Python | Django | Status |
162
+ |---|---|---|
163
+ | 3.11 / 3.12 / 3.13 / 3.14 | 4.2 LTS | ✅ planned |
164
+ | 3.11 / 3.12 / 3.13 / 3.14 | 5.2 LTS | ✅ planned |
165
+ | 3.13 / 3.14 | 6.0 | ✅ planned |
166
+
167
+ Versioning follows [DjangoVer](https://www.b-list.org/weblog/2024/nov/18/djangover/): `<DJANGO_MAJOR>.<DJANGO_FEATURE>.<PACKAGE_VERSION>`. Example: `6.0.1` means "works with Django 6.0, package iteration 1".
168
+
169
+ 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`.
170
+
171
+ ## Comparison
172
+
173
+ | Library | Per-object | Graph traversal | SpiceDB-compatible | AI-agent pattern | Maintained |
174
+ |---|:-:|:-:|:-:|:-:|:-:|
175
+ | `django-zed-rebac` | ✅ | ✅ | ✅ | ✅ Grant pattern | (in development) |
176
+ | `django-guardian` | ✅ (ACL via GenericFK) | ❌ | ❌ | ❌ | ✅ |
177
+ | `django-rules` | ❌ (predicate engine) | ❌ | ❌ | ❌ | ✅ |
178
+ | `django-spicedb` | proxy only | ✅ (via SpiceDB) | ✅ | ❌ | ❌ (early/inactive) |
179
+ | `casbin-django-orm-adapter` | ✅ (ACL, in-memory) | partial | ❌ | ❌ | ✅ |
180
+ | `django-oso` | ✅ | ✅ | ❌ | ❌ | ❌ (deprecated 2023) |
181
+ | `django-rls` (PostgreSQL RLS) | ✅ | DB-level only | ❌ | ❌ | ✅ |
182
+ | `zanzipy` | ✅ | ✅ | partial | ❌ | ❌ (early/single-author) |
183
+
184
+ `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.
185
+
186
+ ## Backends in detail
187
+
188
+ ```
189
+ ┌─ Your application ──────────────────────────────────────────────┐
190
+ │ RebacMixin / RebacPermission / @rebac_resource / @rebac_mcp_tool │
191
+ │ │ │
192
+ │ ┌───────────────▼──────────────────┐ │
193
+ │ │ rebac.backends.Backend (ABC) │ │
194
+ │ │ check_access has_access │ │
195
+ │ │ accessible lookup_subjects │ │
196
+ │ └───────────────┬──────────────────┘ │
197
+ │ ┌─────────────┴────────────┐ │
198
+ │ │ │ │
199
+ │ ┌──────────▼──────────┐ ┌───────────▼───────────┐ │
200
+ │ │ LocalBackend │ │ SpiceDBBackend │ │
201
+ │ │ recursive CTE + │ │ authzed.api.v1.Client │ │
202
+ │ │ cel-python caveats │ │ → gRPC to spicedb │ │
203
+ │ └─────────────────────┘ └────────────────────────┘ │
204
+ └──────────────────────────────────────────────────────────────────┘
205
+ ```
206
+
207
+ 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 (`Zookie`s) are not portable across the swap; this is the only operational consideration documented prominently in [ARCHITECTURE.md § Migration safety](./docs/ARCHITECTURE.md#migration-safety).
208
+
209
+ ## What `django-zed-rebac` is NOT
210
+
211
+ - **Not a User model.** Use `django.contrib.auth.models.User` or any swappable `AUTH_USER_MODEL`.
212
+ - **Not an authentication system.** Use `django-allauth`, `dj-rest-auth`, `simple-jwt`, or your own.
213
+ - **Not a session manager.** Django's session middleware is fine.
214
+ - **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](./docs/ARCHITECTURE.md).)
215
+ - **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.
216
+ - **Not a policy DSL** like Polar or Cedar. The schema language is SpiceDB's `.zed`, REBAC-first. ABAC fragments are expressed via caveats.
217
+
218
+ ## Status & roadmap
219
+
220
+ This is a **pre-alpha** package. The architecture is settled (see [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)) but no PyPI release exists yet. Milestones:
221
+
222
+ - **0.1** — `LocalBackend` MVP, schema parser + sync command, `RebacMixin`, system checks, sync/check commands.
223
+ - **0.2** — Caveats + expiration support.
224
+ - **0.3** — Celery propagation + `ActorMiddleware`.
225
+ - **0.4** — Override layer (`SchemaOverride` + admin + `effective_expr` composition).
226
+ - **0.5** — `SpiceDBBackend` via `authzed-py`.
227
+ - **0.6** — MCP / GraphQL adapters.
228
+ - **1.0** — Stable release with full docs and CI matrix green.
229
+
230
+ Track the full plan in [docs/ARCHITECTURE.md § Roadmap](./docs/ARCHITECTURE.md#roadmap).
231
+
232
+ ## Contributing
233
+
234
+ Once the package lands on PyPI, contribution guidelines will live at `CONTRIBUTING.md`. For now, design feedback is welcome via GitHub issues — schema-language proposals, missing scenarios, integration-surface concerns, anything in [ARCHITECTURE.md § Open questions](./docs/ARCHITECTURE.md#open-questions) you'd push back on.
235
+
236
+ ## License
237
+
238
+ Apache-2.0 (planned, matches `authzed-py`, `cel-python`, and `spicedb` itself).
239
+
240
+ ## Acknowledgments
241
+
242
+ `django-zed-rebac` is a faithful Django port of the model described in [Google's Zanzibar paper](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/) and as implemented by [SpiceDB](https://github.com/authzed/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](https://authzed.com/docs/spicedb/tutorials/ai-agent-authorization). The unified check API (`check_access` / `has_access` / `accessible`) is borrowed from [Odoo 18 PR #179148](https://github.com/odoo/odoo/pull/179148). The `noupdate=True` upgrade-safety semantic is borrowed from Odoo's `ir.model.data`. Caveat evaluation in `LocalBackend` uses [`cel-python`](https://github.com/cloud-custodian/cel-python).
@@ -0,0 +1,207 @@
1
+ # django-zed-rebac
2
+
3
+ > 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.
4
+
5
+ ---
6
+
7
+ > **Status: pre-alpha.** The architecture is settled and reviewed; Tier-1 source has landed (`LocalBackend`, `RebacMixin`/manager, schema parser, `rebac sync` command, system checks). Caveat evaluation, `SpiceDBBackend`, and adapter modules are in flight. Nothing on PyPI yet. Track the milestones at [docs/ARCHITECTURE.md § Roadmap](./docs/ARCHITECTURE.md#roadmap).
8
+
9
+ ---
10
+
11
+ ## What it is
12
+
13
+ `django-zed-rebac` ports [SpiceDB](https://github.com/authzed/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).
14
+
15
+ Two backends are interchangeable behind one Python API:
16
+
17
+ - **`LocalBackend`** — pure Django, evaluates permissions via recursive CTEs against a single `Relationship` table. Zero external infrastructure. Suitable up to ~10M relationships and depth ≤ 8.
18
+ - **`SpiceDBBackend`** — wraps the official [`authzed`](https://pypi.org/project/authzed/) Python client. Connects to a SpiceDB cluster. Drop-in swap when `LocalBackend` is no longer enough — same Python API, no code changes, just `REBAC_BACKEND = "spicedb"` in settings.
19
+
20
+ 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.
21
+
22
+ ## Quickstart
23
+
24
+ ```python
25
+ # settings.py
26
+ INSTALLED_APPS = [
27
+ "django.contrib.auth",
28
+ "django.contrib.contenttypes",
29
+ # ...
30
+ "rebac",
31
+ "blog",
32
+ ]
33
+
34
+ AUTHENTICATION_BACKENDS = [
35
+ "rebac.backends.RebacBackend",
36
+ "django.contrib.auth.backends.ModelBackend",
37
+ ]
38
+
39
+ REBAC_BACKEND = "local"
40
+ ```
41
+
42
+ ```zed
43
+ // blog/permissions.zed
44
+ // @rebac_package: blog
45
+ // @rebac_package_version: 0.1.0
46
+ // @rebac_schema_revision: 1
47
+
48
+ definition blog/post {
49
+ relation owner: auth/user
50
+ relation viewer: auth/user | auth/group#member | auth/user:*
51
+
52
+ permission read = owner + viewer
53
+ permission write = owner
54
+ permission delete = owner
55
+ }
56
+ ```
57
+
58
+ ```python
59
+ # blog/apps.py
60
+ class BlogConfig(AppConfig):
61
+ name = "blog"
62
+ rebac_schema = "permissions.zed" # relative to the app's package dir
63
+
64
+ # blog/models.py
65
+ from django.db import models
66
+ from rebac import RebacMixin
67
+
68
+ class Post(RebacMixin, models.Model):
69
+ title = models.CharField(max_length=200)
70
+ body = models.TextField()
71
+ author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
72
+
73
+ class Meta:
74
+ rebac_resource_type = "blog/post"
75
+ ```
76
+
77
+ ```bash
78
+ python manage.py migrate # creates Relationship + Schema* tables
79
+ python manage.py rebac sync # loads permissions.zed into Schema* tables
80
+ ```
81
+
82
+ ```python
83
+ # blog/views.py
84
+ def post_detail(request, pk):
85
+ post = get_object_or_404(Post.objects.with_actor(request.user), pk=pk)
86
+ return render(request, "post.html", {"post": post})
87
+ ```
88
+
89
+ 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.
90
+
91
+ ## Documentation
92
+
93
+ | Doc | When to read it |
94
+ |---|---|
95
+ | **[docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)** | You're integrating, contributing, or evaluating fit. Architecture, public API, the three storage tiers, settings, surface integrations, determinism, testing, roadmap. |
96
+ | **[docs/ZED.md](./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. |
97
+
98
+ ## Why use this
99
+
100
+ | Problem | Existing options | What `django-zed-rebac` does |
101
+ |---|---|---|
102
+ | 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. |
103
+ | 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`. |
104
+ | 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. |
105
+ | 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. |
106
+ | 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 explicit `.sudo(reason=...)` and is logged. |
107
+ | 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`. |
108
+
109
+ ## Highlights
110
+
111
+ - **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](./docs/ARCHITECTURE.md#conceptual-model).
112
+ - **Unified check API.** `check_access(op)` / `has_access(op)` / `accessible(op)` — one entrypoint family, borrowed from [Odoo 18's PR #179148](https://github.com/odoo/odoo/pull/179148) unification. No model-level vs record-level split at the call site.
113
+ - **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.
114
+ - **`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.
115
+ - **Strict by default.** A queryset without an actor scope raises rather than leaking. Bypass requires explicit `.sudo(reason="cron.expire_drafts")` and writes a structured audit event.
116
+ - **Drop-in DRF integration.** `permission_classes = [RebacPermission]` + `filter_backends = [RebacFilterBackend]`. Per-action permission map; customisable.
117
+ - **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.
118
+ - **MCP-aware.** `@rebac_mcp_tool` decorator wraps FastMCP / official-SDK tool functions; resolves the actor from `ctx.request_context.meta`; checks before the tool body runs.
119
+ - **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.
120
+ - **Type-checked.** Ships `py.typed`. `RebacManager[M]` is `Generic[M]` — your IDE infers the right model class through the manager. `mypy --strict` and `pyright` both run on CI.
121
+ - **`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.
122
+ - **Deterministic build.** `python manage.py rebac sync --check` is a CI gate that returns non-zero on schema drift, mirroring `migrate --check`.
123
+
124
+ ## Compatibility
125
+
126
+ | Python | Django | Status |
127
+ |---|---|---|
128
+ | 3.11 / 3.12 / 3.13 / 3.14 | 4.2 LTS | ✅ planned |
129
+ | 3.11 / 3.12 / 3.13 / 3.14 | 5.2 LTS | ✅ planned |
130
+ | 3.13 / 3.14 | 6.0 | ✅ planned |
131
+
132
+ Versioning follows [DjangoVer](https://www.b-list.org/weblog/2024/nov/18/djangover/): `<DJANGO_MAJOR>.<DJANGO_FEATURE>.<PACKAGE_VERSION>`. Example: `6.0.1` means "works with Django 6.0, package iteration 1".
133
+
134
+ 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`.
135
+
136
+ ## Comparison
137
+
138
+ | Library | Per-object | Graph traversal | SpiceDB-compatible | AI-agent pattern | Maintained |
139
+ |---|:-:|:-:|:-:|:-:|:-:|
140
+ | `django-zed-rebac` | ✅ | ✅ | ✅ | ✅ Grant pattern | (in development) |
141
+ | `django-guardian` | ✅ (ACL via GenericFK) | ❌ | ❌ | ❌ | ✅ |
142
+ | `django-rules` | ❌ (predicate engine) | ❌ | ❌ | ❌ | ✅ |
143
+ | `django-spicedb` | proxy only | ✅ (via SpiceDB) | ✅ | ❌ | ❌ (early/inactive) |
144
+ | `casbin-django-orm-adapter` | ✅ (ACL, in-memory) | partial | ❌ | ❌ | ✅ |
145
+ | `django-oso` | ✅ | ✅ | ❌ | ❌ | ❌ (deprecated 2023) |
146
+ | `django-rls` (PostgreSQL RLS) | ✅ | DB-level only | ❌ | ❌ | ✅ |
147
+ | `zanzipy` | ✅ | ✅ | partial | ❌ | ❌ (early/single-author) |
148
+
149
+ `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.
150
+
151
+ ## Backends in detail
152
+
153
+ ```
154
+ ┌─ Your application ──────────────────────────────────────────────┐
155
+ │ RebacMixin / RebacPermission / @rebac_resource / @rebac_mcp_tool │
156
+ │ │ │
157
+ │ ┌───────────────▼──────────────────┐ │
158
+ │ │ rebac.backends.Backend (ABC) │ │
159
+ │ │ check_access has_access │ │
160
+ │ │ accessible lookup_subjects │ │
161
+ │ └───────────────┬──────────────────┘ │
162
+ │ ┌─────────────┴────────────┐ │
163
+ │ │ │ │
164
+ │ ┌──────────▼──────────┐ ┌───────────▼───────────┐ │
165
+ │ │ LocalBackend │ │ SpiceDBBackend │ │
166
+ │ │ recursive CTE + │ │ authzed.api.v1.Client │ │
167
+ │ │ cel-python caveats │ │ → gRPC to spicedb │ │
168
+ │ └─────────────────────┘ └────────────────────────┘ │
169
+ └──────────────────────────────────────────────────────────────────┘
170
+ ```
171
+
172
+ 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 (`Zookie`s) are not portable across the swap; this is the only operational consideration documented prominently in [ARCHITECTURE.md § Migration safety](./docs/ARCHITECTURE.md#migration-safety).
173
+
174
+ ## What `django-zed-rebac` is NOT
175
+
176
+ - **Not a User model.** Use `django.contrib.auth.models.User` or any swappable `AUTH_USER_MODEL`.
177
+ - **Not an authentication system.** Use `django-allauth`, `dj-rest-auth`, `simple-jwt`, or your own.
178
+ - **Not a session manager.** Django's session middleware is fine.
179
+ - **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](./docs/ARCHITECTURE.md).)
180
+ - **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.
181
+ - **Not a policy DSL** like Polar or Cedar. The schema language is SpiceDB's `.zed`, REBAC-first. ABAC fragments are expressed via caveats.
182
+
183
+ ## Status & roadmap
184
+
185
+ This is a **pre-alpha** package. The architecture is settled (see [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)) but no PyPI release exists yet. Milestones:
186
+
187
+ - **0.1** — `LocalBackend` MVP, schema parser + sync command, `RebacMixin`, system checks, sync/check commands.
188
+ - **0.2** — Caveats + expiration support.
189
+ - **0.3** — Celery propagation + `ActorMiddleware`.
190
+ - **0.4** — Override layer (`SchemaOverride` + admin + `effective_expr` composition).
191
+ - **0.5** — `SpiceDBBackend` via `authzed-py`.
192
+ - **0.6** — MCP / GraphQL adapters.
193
+ - **1.0** — Stable release with full docs and CI matrix green.
194
+
195
+ Track the full plan in [docs/ARCHITECTURE.md § Roadmap](./docs/ARCHITECTURE.md#roadmap).
196
+
197
+ ## Contributing
198
+
199
+ Once the package lands on PyPI, contribution guidelines will live at `CONTRIBUTING.md`. For now, design feedback is welcome via GitHub issues — schema-language proposals, missing scenarios, integration-surface concerns, anything in [ARCHITECTURE.md § Open questions](./docs/ARCHITECTURE.md#open-questions) you'd push back on.
200
+
201
+ ## License
202
+
203
+ Apache-2.0 (planned, matches `authzed-py`, `cel-python`, and `spicedb` itself).
204
+
205
+ ## Acknowledgments
206
+
207
+ `django-zed-rebac` is a faithful Django port of the model described in [Google's Zanzibar paper](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/) and as implemented by [SpiceDB](https://github.com/authzed/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](https://authzed.com/docs/spicedb/tutorials/ai-agent-authorization). The unified check API (`check_access` / `has_access` / `accessible`) is borrowed from [Odoo 18 PR #179148](https://github.com/odoo/odoo/pull/179148). The `noupdate=True` upgrade-safety semantic is borrowed from Odoo's `ir.model.data`. Caveat evaluation in `LocalBackend` uses [`cel-python`](https://github.com/cloud-custodian/cel-python).
@@ -0,0 +1,91 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "django-zed-rebac"
7
+ version = "0.1.4"
8
+ description = "Drop-in REBAC engine for Django, SpiceDB-compatible, with a pure-Django LocalBackend."
9
+ readme = "README.md"
10
+ license = { text = "Apache-2.0" }
11
+ requires-python = ">=3.14"
12
+ authors = [{ name = "Apexive", email = "hi@apexive.com" }]
13
+ keywords = ["django", "rebac", "spicedb", "zanzibar", "authorization", "permissions"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Framework :: Django",
17
+ "Framework :: Django :: 4.2",
18
+ "Framework :: Django :: 5.2",
19
+ "Framework :: Django :: 6.0",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: Apache Software License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Security",
25
+ ]
26
+ dependencies = [
27
+ "django>=4.2",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ spicedb = ["authzed>=0.13"]
32
+ caveats = ["cel-python>=0.1.5"]
33
+ drf = ["djangorestframework>=3.14"]
34
+ dev = [
35
+ "pytest>=8.0",
36
+ "pytest-django>=4.8",
37
+ "ruff>=0.5",
38
+ "mypy>=1.10",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/apexive/django-zed-rebac"
43
+ Documentation = "https://github.com/apexive/django-zed-rebac/tree/main/docs"
44
+ Issues = "https://github.com/apexive/django-zed-rebac/issues"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+ include = ["rebac*"]
49
+
50
+ [tool.setuptools.package-data]
51
+ rebac = ["py.typed"]
52
+
53
+ [tool.pytest.ini_options]
54
+ DJANGO_SETTINGS_MODULE = "tests.settings"
55
+ python_files = ["test_*.py"]
56
+ testpaths = ["tests"]
57
+ pythonpath = ["."]
58
+ filterwarnings = [
59
+ "error",
60
+ "ignore::DeprecationWarning:django.*",
61
+ ]
62
+
63
+ [tool.ruff]
64
+ line-length = 100
65
+ target-version = "py314"
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "I", "B", "UP", "RUF"]
69
+ ignore = ["E501"]
70
+
71
+ [tool.mypy]
72
+ python_version = "3.14"
73
+ strict = true
74
+ plugins = []
75
+ exclude = ["tests/", "build/"]
76
+
77
+ [[tool.mypy.overrides]]
78
+ module = ["authzed.*", "celery.*", "rest_framework.*"]
79
+ ignore_missing_imports = true
80
+
81
+
82
+ [tool.ruff.lint.per-file-ignores]
83
+ # Lazy `__getattr__` shim plus grouped (not alphabetised) `__all__` is intentional;
84
+ # see CLAUDE.md § "Don't import ZedRBACMixin from `__init__.py` eagerly".
85
+ "src/rebac/__init__.py" = ["E402", "RUF022"]
86
+ # Django Meta options (indexes, constraints, ordering, operations) MUST be class-level
87
+ # lists/sets/tuples — that's the framework's convention.
88
+ "src/rebac/admin.py" = ["RUF012"]
89
+ "src/rebac/models/*.py" = ["RUF012"]
90
+ "src/rebac/migrations/*.py" = ["RUF012"]
91
+ "tests/testapp/migrations/*.py" = ["I001", "RUF012"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,63 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ src/rebac/__init__.py
5
+ src/rebac/_id.py
6
+ src/rebac/actors.py
7
+ src/rebac/admin.py
8
+ src/rebac/apps.py
9
+ src/rebac/audit.py
10
+ src/rebac/caveats.py
11
+ src/rebac/checks.py
12
+ src/rebac/codenames.py
13
+ src/rebac/composition.py
14
+ src/rebac/conf.py
15
+ src/rebac/decorators.py
16
+ src/rebac/drf.py
17
+ src/rebac/errors.py
18
+ src/rebac/managers.py
19
+ src/rebac/middleware.py
20
+ src/rebac/mixins.py
21
+ src/rebac/permissions_mixin.py
22
+ src/rebac/py.typed
23
+ src/rebac/relationships.py
24
+ src/rebac/resources.py
25
+ src/rebac/signals.py
26
+ src/rebac/types.py
27
+ src/rebac/backends/__init__.py
28
+ src/rebac/backends/auth.py
29
+ src/rebac/backends/base.py
30
+ src/rebac/backends/local.py
31
+ src/rebac/backends/spicedb.py
32
+ src/rebac/management/__init__.py
33
+ src/rebac/management/commands/__init__.py
34
+ src/rebac/management/commands/rebac.py
35
+ src/rebac/migrations/0001_initial.py
36
+ src/rebac/migrations/__init__.py
37
+ src/rebac/models/__init__.py
38
+ src/rebac/models/audit.py
39
+ src/rebac/models/overrides.py
40
+ src/rebac/models/provenance.py
41
+ src/rebac/models/relationship.py
42
+ src/rebac/models/schema.py
43
+ src/rebac/schema/__init__.py
44
+ src/rebac/schema/ast.py
45
+ src/rebac/schema/parser.py
46
+ tests/test_actors.py
47
+ tests/test_audit.py
48
+ tests/test_auth_backend.py
49
+ tests/test_build_zed.py
50
+ tests/test_caveats.py
51
+ tests/test_checks_and_conf.py
52
+ tests/test_codenames.py
53
+ tests/test_field_gates.py
54
+ tests/test_id_attr.py
55
+ tests/test_local_backend.py
56
+ tests/test_middleware.py
57
+ tests/test_mixin.py
58
+ tests/test_overrides.py
59
+ tests/test_parser.py
60
+ tests/test_permissions_mixin.py
61
+ tests/test_pickle_posture.py
62
+ tests/test_rebac_object_meta.py
63
+ tests/test_types.py