Skip to main content

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->permission expressions
  • 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_spicedb',
]

Configure SpiceDB connection:

REBAC = {
    'adapter': {
        'endpoint': 'localhost:50051',
        'token': 'your-spicedb-token',
        'insecure': True,  # For local development
    },
}

Quickstart

This walkthrough models a tiny system and shows the thinking process you'll repeat in real apps: rules → relations → permissions → code.

0. Start SpiceDB

docker run -d --name spicedb -p 50051:50051 \
  authzed/spicedb serve --grpc-preshared-key devkey

1. Write the rules in plain English

Pick a concrete example. We'll use folders and documents:

  • A folder can have a parent folder.
  • A document has an owner and can live in a folder.
  • Owners can view/edit their own items.
  • If you can view/edit a parent folder, you can view/edit its children.

If you can explain the rule to a teammate, you can model it.

2. Turn rules into relations and permissions

Think in two layers:

  • Relations: direct links like "document.owner" or "folder.parent"
  • Permissions: expressions like "view = owner + parent->view"

For our example:

  • Relations: owner, parent
  • Permissions: view = owner + parent->view, edit = owner + parent->edit

3. Implement models with RebacMeta

Now we express that mapping in Django models.

from django.db import models
from django_spicedb.models import RebacModel
from django_spicedb.integrations.orm import RebacManager

class Folder(RebacModel):
    name = models.CharField(max_length=255)
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)

    objects = RebacManager()

    class RebacMeta:
        relations = {
            'owner': 'owner',
            'parent': 'parent',
        }
        permissions = {
            'view': 'owner + parent->view',
            'edit': 'owner + parent->edit',
        }

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',
            'parent': 'folder',
        }
        permissions = {
            'view': 'owner + parent->view',
            'edit': 'owner + parent->edit',
        }

4. Publish schema and backfill tuples

Schema tells SpiceDB what the types and permissions look like. Backfill creates the initial tuples from existing rows.

python manage.py publish_rebac_schema
python manage.py rebac_backfill

5. Use permissions in code

Now you can ask SpiceDB whether a user can access an object, or filter querysets by permission.

from django_spicedb.runtime import can

if can(request.user, 'view', document):
    # User can view this document

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_spicedb.models import RebacModel
from django_spicedb.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_spicedb.adapters import factory
from django_spicedb.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 fieldskind: 'fk' - Uses field_id cache, tracks changes
  • M2M fieldskind: 'm2m' - Syncs on post_add, post_remove, post_clear
  • Manual{'subject': 'type'} - No auto-sync, handle via signals

How It Works

  1. Schema Generation: RebacMeta on models compiles to SpiceDB schema DSL
  2. Tuple Sync: Django signals (post_save, post_delete, m2m_changed) write/delete tuples
  3. FK Tracking: pre_save captures old FK values; post_save deletes stale tuples
  4. Transaction Safety: All SpiceDB writes happen in transaction.on_commit()
  5. 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

# Reconcile tuples (re-writes all expected tuples, idempotent)
python manage.py rebac_reconcile --fix

# Export policy to YAML (requires pyyaml)
pip install django-spicedb[yaml]
python manage.py export_rebac_policy

DRF Integration

Requires djangorestframework:

pip install django-spicedb[drf]

Object-Level Permissions

from rest_framework.viewsets import ModelViewSet
from django_spicedb.drf import ReBACPermission

class DocumentViewSet(ModelViewSet):
    queryset = Document.objects.all()
    permission_classes = [ReBACPermission]

    # Single permission for all actions:
    rebac_permission = "view"

    # Or per-action:
    rebac_action_permissions = {
        "list": "view",
        "retrieve": "view",
        "update": "edit",
        "destroy": "delete",
    }

Queryset Filtering

from django_spicedb.drf import ReBACFilterBackend

class DocumentViewSet(ModelViewSet):
    queryset = Document.objects.all()
    filter_backends = [ReBACFilterBackend]
    rebac_filter_permission = "view"  # defaults to "view"

Testing

django-spicedb ships a pytest plugin that spins up an ephemeral SpiceDB container via testcontainers. No Docker Compose, no shared state between runs.

pip install django-spicedb[test]

The plugin registers three session-scoped fixtures automatically (no conftest boilerplate needed):

Fixture Scope Description
spicedb_container session Starts an in-memory SpiceDB (serve-testing)
spicedb_grpc_endpoint session Returns the host:port string
spicedb_adapter session A SpiceDBAdapter wired to the container; publishes schema on creation and overrides the global adapter

Usage

def test_owner_can_view(spicedb_adapter):
    """spicedb_adapter is already connected and the schema is published."""
    from django_spicedb.adapters.base import TupleKey, TupleWrite

    spicedb_adapter.write_tuples([
        TupleWrite(key=TupleKey(
            object="document:1",
            relation="owner",
            subject="user:42",
        ))
    ])

    assert spicedb_adapter.check("user:42", "view", "document:1")

If testcontainers is not installed the fixtures pytest.skip() automatically, so your unit tests still run without Docker.


Full Tutorial

Want a complete walkthrough? Read the tutorial - builds a document management system step-by-step, explaining every concept along the way.


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


Download files

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

Source Distribution

django_spicedb-0.3.1.tar.gz (61.2 kB view details)

Uploaded Source

Built Distribution

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

django_spicedb-0.3.1-py3-none-any.whl (82.3 kB view details)

Uploaded Python 3

File details

Details for the file django_spicedb-0.3.1.tar.gz.

File metadata

  • Download URL: django_spicedb-0.3.1.tar.gz
  • Upload date:
  • Size: 61.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

Hashes for django_spicedb-0.3.1.tar.gz
Algorithm Hash digest
SHA256 58daeb6a74ba8c7ac6c10539c76472abdacfc90e0cf36e0369058cb02e9babba
MD5 a56e4e22f84da61c9df3ca5440714c96
BLAKE2b-256 3fb65f8f7a70e6ac13fdb6d76b58944f04e6897de1009a07fb73c1d047cecd63

See more details on using hashes here.

File details

Details for the file django_spicedb-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: django_spicedb-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 82.3 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

Hashes for django_spicedb-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4ebe91f177e3e90aaa787c75998c7dbe722f16e5e1b96efb5a012c97e16e05e2
MD5 7291af3a084692e88ba2ffaaedcae1b6
BLAKE2b-256 93956305823a99650486ec0cf4ca62d554f21b42956b7b32f3e6a18efdd2026d

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