Skip to content

Mixins API Reference

mixins

Classes

RebacViewMixin

RebacViewMixin(*args: Any, **kwargs: Any)

Structure-agnostic mixin for DRF Views to enforce ReBAC Authorization.

This mixin automatically handles three critical authorization lifecycle hooks in Django REST Framework without requiring manual permission logic:

  1. Queryset Filtering (get_queryset): Injects id__in filters on list views.
  2. Parent Verification (check_permissions): Checks required parent roles on POST requests.
  3. Object Verification (check_object_permissions): Checks specific object roles on PUT/PATCH/DELETE.

Attributes:

Name Type Description
rebac_config RebacViewConfig | None

The strict configuration class defining the authorization rules for this view. Must be an instance of RebacViewConfig.

Example
from rest_framework import viewsets
from rebac_data_sync.structs import RebacViewConfig

class DocumentViewSet(RebacViewMixin, viewsets.ModelViewSet):
    queryset = Document.objects.all()
    serializer_class = DocumentSerializer

    rebac_config = RebacViewConfig(
        object_type="document",
        read_relation="can_read_document",
        update_relation="can_update",
        delete_relation="can_delete"
    )

Raises:

Type Description
ImproperlyConfigured

If rebac_config is missing, invalid, or utilizes ViewSet-only features (like action_relations) on a standard Generic View.

AuthenticationFailed

If the Traefik identity header is missing.

PermissionDenied

If the OpenFGA network check denies access.

Source code in src/rebac/views/mixins.py
def __init__(self, *args: Any, **kwargs: Any) -> None:
    super().__init__(*args, **kwargs)

    # 🛡️ THE GUARDRAIL: Check for duplicate authorization strategies
    from ..permissions import IsRebacAuthorized

    permission_classes = getattr(self, "permission_classes", [])
    if IsRebacAuthorized in permission_classes:
        # Warning: The developer applied redundant permission checks!
        dev_logger.warning(
            f"Duplicate ReBAC Authorization detected on '{self.__class__.__name__}'. "
            f"You are using both 'RebacViewMixin' and 'IsRebacAuthorized'. "
            f"This will result in redundant network calls. "
            f"Please remove 'IsRebacAuthorized' from 'permission_classes'."
        )
Functions
get_serializer_context
get_serializer_context() -> dict[str, Any]

DRF hook: Injects ReBAC tools directly into the serializer context.

Source code in src/rebac/views/mixins.py
def get_serializer_context(self) -> dict[str, Any]:
    """DRF hook: Injects ReBAC tools directly into the serializer context."""
    context = super().get_serializer_context()  # type: ignore[misc]

    user_attr = get_setting("REBAC_USER_ATTR")
    rebac_user = getattr(self.request, user_attr, None)

    if rebac_user:
        context["rebac_user"] = rebac_user
        context["rebac_client"] = get_rebac_client()

    return context

Functions

mixins

Classes

RebacModelSyncMixin

RebacModelSyncMixin(*args: Any, **kwargs: Any)

Structure-agnostic mixin for synchronizing Django models to the ReBAC store via the Outbox pattern.

This mixin intercepts the standard Django save() and delete() lifecycles. It utilizes the defined RebacModelConfig to calculate the exact ReBAC tuple differences (diffs) and safely queues them in the local database transaction.

Attributes:

Name Type Description
rebac_config RebacModelConfig | None

The strict configuration class defining how this model maps to the OpenFGA graph. Must be an instance of RebacModelConfig.

pk int | str | UUID | None

The primary key of the model instance.

Example

Basic Creator Ownership:

from django.db import models
from rebac.structs import RebacModelConfig, RebacCreatorConfig

class Document(RebacModelSyncMixin, models.Model):
    title = models.CharField(max_length=255)
    creator_id = models.CharField(max_length=255)

    rebac_config = RebacModelConfig(
        object_type="document",
        creators=[
            RebacCreatorConfig(
                relation="editor",
                local_field="creator_id"
            )
        ]
    )

Parent Hierarchies (Cascading):

from rebac.structs import RebacParentConfig

class Folder(RebacModelSyncMixin, models.Model):
    name = models.CharField(max_length=255)
    org_id = models.CharField(max_length=255)
    creator_id = models.CharField(max_length=255)

    rebac_config = RebacModelConfig(
        object_type="folder",
        parents=[
            RebacParentConfig(
                relation="organization",
                parent_type="organization",
                local_field="org_id"
            )
        ],
        creators=[
            RebacCreatorConfig(
                relation="owner",
                local_field="creator_id"
            )
        ]
    )

Custom Role Assignment (Escape Hatch): If you need to assign ReBAC roles based on dynamic data state (like a boolean field), you can intercept save() and manually queue tuples into the Outbox:

from rebac.models import RebacSyncOutbox

class Article(RebacModelSyncMixin, models.Model):
    title = models.CharField(max_length=255)
    is_public = models.BooleanField(default=False)

    rebac_config = RebacModelConfig(object_type="article")

    def save(self, *args, **kwargs):
        # 1. Let the mixin handle the standard config-based tuples first
        super().save(*args, **kwargs)

        # 2. Inject your custom, dynamic logic
        if self.is_public:
            self._queue_outbox(
                action=RebacSyncOutbox.Action.WRITE,
                t={
                    "user": "user:*",  # OpenFGA wildcard for 'everyone'
                    "relation": "viewer",
                    "object": f"article:{self.pk}"
                }
            )

Notes

Limitations: Because this mixin relies on intercepting the save() method for the Transactional Outbox pattern, standard Django bulk operations (e.g., Document.objects.bulk_create()) will bypass this mixin. You must save instances individually or trigger the outbox manually for bulk operations.

Source code in src/rebac/models/mixins.py
def __init__(self, *args: Any, **kwargs: Any) -> None:
    super().__init__(*args, **kwargs)

    # Fail fast on instantiation if misconfigured
    config = self._get_validated_config()

    # Delegate tuple generation to the adapter
    self._original_tuples = RebacTupleAdapter.generate_tuples(self, config) if self.pk else []
    self._rebac_task_scheduled = False
Functions
__init_subclass__ classmethod
__init_subclass__(**kwargs: Any) -> None

Metaprogramming Hook: Executed automatically when a Django model inherits from this mixin. Wires up framework-level safety nets.

Source code in src/rebac/models/mixins.py
@classmethod
def __init_subclass__(cls, **kwargs: Any) -> None:
    """
    Metaprogramming Hook: Executed automatically when a Django model inherits
    from this mixin. Wires up framework-level safety nets.
    """
    super().__init_subclass__(**kwargs)

    # Generate a unique dispatch_uid to prevent duplicate signal registration
    # if the module is reloaded (e.g., during tests or dev server reloads).
    dispatch_uid = f"{cls.__module__}.{cls.__name__}.rebac_auto_cascade"
    pre_delete.connect(cls._rebac_auto_cascade_handler, sender=cls, dispatch_uid=dispatch_uid)
delete
delete(*args: Any, **kwargs: Any) -> None

Standard instance deletion override.

Source code in src/rebac/models/mixins.py
def delete(self, *args: Any, **kwargs: Any) -> None:
    """Standard instance deletion override."""
    with transaction.atomic():
        config = self._get_validated_config()

        # 1. Set the flag so the auto-handler knows we already caught it
        self._rebac_is_deleting = True

        # 2. Queue the deletions
        for t in RebacTupleAdapter.generate_tuples(self, config):
            self._queue_outbox(RebacSyncOutbox.Action.DELETE, t)  # type: ignore[arg-type]

        # 3. Proceed with Django's native deletion
        super().delete(*args, **kwargs)  # type: ignore[misc]

👉 See the Model Syncing Guide for full tutorials on Multi-Parent and Multi-Creator architectures.