Declarative relationship-based access control (ReBAC) for Django, backed by SpiceDB
Project description
django-spicedb
Declarative, relationship-based access control (ReBAC) for Django, backed by SpiceDB. Define your authorization model once in Python; let Django enforce it everywhere.
Features
- Model-centric configuration - Define relations and permissions directly on Django models via
RebacMeta - Automatic tuple sync - FK and M2M changes automatically sync to SpiceDB via signals
- Permission inheritance - Build hierarchies with
parent->permissionexpressions - Group-based access - Role-based group membership (member/manager) out of the box
- Query integration - Filter querysets by permission with
.accessible_by(user, 'view') - FK change tracking - Automatically cleans up stale tuples when relationships change
Installation
pip install django-spicedb
Add to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
'django_rebac',
]
Configure SpiceDB connection:
REBAC = {
'adapter': {
'endpoint': 'localhost:50051',
'token': 'your-spicedb-token',
'insecure': True, # For local development
},
}
Quickstart
1. Start SpiceDB
docker run -d --name spicedb -p 50051:50051 \
authzed/spicedb serve --grpc-preshared-key devkey
2. Define Your Models
from django.db import models
from django_rebac.models import RebacModel
from django_rebac.integrations.orm import RebacManager
class Document(RebacModel):
title = models.CharField(max_length=255)
owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
folder = models.ForeignKey('Folder', on_delete=models.CASCADE, null=True)
objects = RebacManager()
class RebacMeta:
relations = {
'owner': 'owner', # FK field name
'parent': 'folder', # FK to parent resource
}
permissions = {
'view': 'owner + parent->view',
'edit': 'owner + parent->edit',
}
3. Publish Schema & Backfill
python manage.py publish_rebac_schema
python manage.py rebac_backfill
4. Check Permissions
from django_rebac.runtime import can
# Simple check
if can(request.user, 'view', document):
# User can view this document
# Query accessible objects
documents = Document.objects.accessible_by(request.user, 'view')
Group-Based Access Control
For team/department-based permissions, use the Group pattern:
from django.db import models
from django_rebac.models import RebacModel
from django_rebac.integrations.orm import RebacManager
class Group(RebacModel):
"""A group with role-based membership."""
name = models.CharField(max_length=255)
objects = RebacManager()
class RebacMeta:
type_name = 'group'
relations = {
# Manual relations - no field binding, synced via GroupMembership
'member': {'subject': 'user'},
'manager': {'subject': 'user'},
}
permissions = {
'view': 'member + manager',
'manage': 'manager',
}
class GroupMembership(models.Model):
"""Through table for group membership with roles."""
ROLE_MEMBER = 'member'
ROLE_MANAGER = 'manager'
ROLE_CHOICES = [
(ROLE_MEMBER, 'Member'),
(ROLE_MANAGER, 'Manager'),
]
group = models.ForeignKey(Group, on_delete=models.CASCADE)
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
role = models.CharField(max_length=32, choices=ROLE_CHOICES, default=ROLE_MEMBER)
class Meta:
constraints = [
models.UniqueConstraint(fields=('group', 'user'), name='unique_membership'),
]
class Verification(RebacModel):
"""A resource that inherits permissions from its parent group."""
title = models.CharField(max_length=255)
owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
objects = RebacManager()
class RebacMeta:
type_name = 'verification'
relations = {
'owner': 'owner',
'parent': 'group',
}
permissions = {
'view': 'owner + parent->view', # Owner OR group members
'manage': 'owner + parent->manage', # Owner OR group managers
}
Then create signal handlers for GroupMembership to sync tuples:
# signals.py
from django.db import transaction
from django.db.models.signals import post_save, post_delete
from django_rebac.adapters import factory
from django_rebac.adapters.base import TupleKey, TupleWrite
def handle_membership_save(sender, instance, **kwargs):
def do_sync():
factory.get_adapter().write_tuples([
TupleWrite(key=TupleKey(
object=f'group:{instance.group_id}',
relation=instance.role,
subject=f'user:{instance.user_id}',
))
])
transaction.on_commit(do_sync)
def handle_membership_delete(sender, instance, **kwargs):
def do_delete():
factory.get_adapter().delete_tuples([
TupleKey(
object=f'group:{instance.group_id}',
relation=instance.role,
subject=f'user:{instance.user_id}',
)
])
transaction.on_commit(do_delete)
post_save.connect(handle_membership_save, sender=GroupMembership)
post_delete.connect(handle_membership_delete, sender=GroupMembership)
Now permissions flow naturally:
- Group members can view all verifications in their group
- Group managers can view and manage all verifications in their group
- Owners always have full access to their own verifications
RebacMeta Reference
Relations
Map Django fields to SpiceDB relations:
class RebacMeta:
relations = {
# FK binding - field name maps to relation
'owner': 'owner_field',
# M2M binding - field name maps to relation
'member': 'members_field',
# Manual relation - no field, synced manually
'manager': {'subject': 'user'},
# Parent relation for hierarchy
'parent': 'parent_folder',
}
Permissions
SpiceDB permission expressions:
class RebacMeta:
permissions = {
'view': 'owner + member', # OR: owner or member
'edit': 'owner', # Direct relation
'admin': 'owner + parent->admin', # Inherited from parent
'manage': 'manager + parent->manage',
}
Binding Kinds
Bindings are auto-inferred from field types:
- FK fields →
kind: 'fk'- Usesfield_idcache, tracks changes - M2M fields →
kind: 'm2m'- Syncs onpost_add,post_remove,post_clear - Manual →
{'subject': 'type'}- No auto-sync, handle via signals
How It Works
- Schema Generation:
RebacMetaon models compiles to SpiceDB schema DSL - Tuple Sync: Django signals (
post_save,post_delete,m2m_changed) write/delete tuples - FK Tracking:
pre_savecaptures old FK values;post_savedeletes stale tuples - Transaction Safety: All SpiceDB writes happen in
transaction.on_commit() - Permission Checks:
can()and.accessible_by()query SpiceDB via gRPC
Management Commands
# Publish schema to SpiceDB
python manage.py publish_rebac_schema
# Backfill tuples from existing Django data
python manage.py rebac_backfill
DRF Integration
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ['django_rebac.drf.ReBACPermission'],
}
Development
See developers.md for development setup, testing, and contributing guidelines.
# Install dependencies
poetry install
# Start SpiceDB
docker compose up -d spicedb
# Run tests
poetry run pytest
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_spicedb-0.1.0.tar.gz.
File metadata
- Download URL: django_spicedb-0.1.0.tar.gz
- Upload date:
- Size: 44.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.1 CPython/3.11.11 Darwin/22.6.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a98b733074d2b399c8132d1e92e0be2fcf290838e7c223c5c3778c705e9df6f5
|
|
| MD5 |
a09f897862a7984243a1be8656e95317
|
|
| BLAKE2b-256 |
21b038d92a7423db1f8b1c409477e9db59eb068c5ec4b1758e3d8142ae4e4013
|
File details
Details for the file django_spicedb-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_spicedb-0.1.0-py3-none-any.whl
- Upload date:
- Size: 59.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.1 CPython/3.11.11 Darwin/22.6.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b41efc7d5ffed551a107dc4460001ef93fcb7083c9cadf8e38134aed0811cad
|
|
| MD5 |
2883853fc00dfe86ac8997934cb5af67
|
|
| BLAKE2b-256 |
334473c9bd7c6ba08f2b7390eec20ae9b80c245fc74578e5d93ec5339f1dafb4
|