Securing API Views
The developer protects the API using the IsRebacAuthorized permission class or the RebacViewMixin.
You do not have to write custom logic to parse identity headers.
The GatewayIdentityMiddleware dynamically extracts your configured gateway headers (e.g., X-User-Id) and attaches them to the request automatically.
Below is the complete implementation for a full CRUD lifecycle across our Organization -> Folder -> Document hierarchy.
1. The Serializers
First, we define our serializers. Because our ReBAC integration intercepts the POST payload to check parent permissions, the parent ID fields (organization_id, folder_id) must be writable fields. The creator_id is always read-only, as our views inject the identity securely from the Traefik middleware.
# serializers.py
from rest_framework import serializers
from .models import Organization, Folder, Document
class OrganizationSerializer(serializers.ModelSerializer):
class Meta:
model = Organization
fields = ['id', 'name', 'creator_id']
read_only_fields = ['creator_id']
class FolderSerializer(serializers.ModelSerializer):
class Meta:
model = Folder
# organization_id is required from the client payload so the
# IsRebacAuthorized permission class can verify parent cascading!
fields = ['id', 'name', 'organization_id', 'creator_id']
read_only_fields = ['creator_id']
class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
# folder_id is required from the client payload!
fields = ['id', 'title', 'content', 'folder_id', 'creator_id']
read_only_fields = ['creator_id']
Method A: Using DRF Generic Views
If you prefer building explicit, single-purpose endpoints, DRF Generic API Views are the way to go. Here is how you configure the rebac_config dataclass for Creation (POST) vs. Detail Mutations (GET/PUT/DELETE).
# views.py (Generics Approach)
from rest_framework import generics
from rebac.permissions import IsRebacAuthorized
from rebac.structs import RebacViewConfig
from .models import Organization, Folder, Document
from .serializers import OrganizationSerializer, FolderSerializer, DocumentSerializer
# ==========================================
# 1. ORGANIZATION VIEWS
# ==========================================
class OrganizationCreateAPIView(generics.CreateAPIView):
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer
permission_classes = [IsRebacAuthorized]
# Top-level entity: No parent check required in this basic setup.
rebac_config = RebacViewConfig(object_type="organization")
def perform_create(self, serializer):
raw_user_id = self.request.rebac_user.replace("user:", "")
serializer.save(creator_id=raw_user_id)
class OrganizationDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer
permission_classes = [IsRebacAuthorized]
rebac_config = RebacViewConfig(
object_type="organization",
read_relation="can_list_org",
update_relation="can_manage_settings",
delete_relation="can_manage_settings"
)
# ==========================================
# 2. FOLDER VIEWS
# ==========================================
class FolderCreateAPIView(generics.CreateAPIView):
queryset = Folder.objects.all()
serializer_class = FolderSerializer
permission_classes = [IsRebacAuthorized]
# 🛡️ Tell the shield what the rules are for Creation
rebac_config = RebacViewConfig(
object_type="folder",
create_scope_type="organization",
create_scope_field="organization_id",
create_relation="can_manage_settings"
)
def perform_create(self, serializer):
raw_user_id = self.request.rebac_user.replace("user:", "")
serializer.save(creator_id=raw_user_id)
class FolderDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Folder.objects.all()
serializer_class = FolderSerializer
permission_classes = [IsRebacAuthorized]
rebac_config = RebacViewConfig(
object_type="folder",
read_relation="can_list_folder",
update_relation="can_edit_folder",
delete_relation="can_edit_folder"
)
# ==========================================
# 3. DOCUMENT VIEWS
# ==========================================
class DocumentCreateAPIView(generics.CreateAPIView):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
permission_classes = [IsRebacAuthorized]
# 🛡️ Tell the shield what the rules are for Creation
rebac_config = RebacViewConfig(
object_type="document",
create_scope_type="folder",
create_scope_field="folder_id",
create_relation="can_add_items"
)
def perform_create(self, serializer):
raw_user_id = self.request.rebac_user.replace("user:", "")
serializer.save(creator_id=raw_user_id)
class DocumentDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
permission_classes = [IsRebacAuthorized]
rebac_config = RebacViewConfig(
object_type="document",
read_relation="can_read_document",
update_relation="can_update",
delete_relation="can_delete"
)
Method B: Using DRF ViewSets (Recommended)
If you prefer building RESTful APIs rapidly with ViewSets and Routers, you can combine all permissions into a single, elegant RebacViewConfig class for each model. The IsRebacAuthorized permission shield intelligently reads the incoming HTTP method and applies the correct check automatically.
# views.py (ViewSet Approach)
from rest_framework import viewsets
from rebac.permissions import IsRebacAuthorized
from rebac.structs import RebacViewConfig
from .models import Organization, Folder, Document
from .serializers import OrganizationSerializer, FolderSerializer, DocumentSerializer
class OrganizationViewSet(viewsets.ModelViewSet):
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer
permission_classes = [IsRebacAuthorized]
rebac_config = RebacViewConfig(
object_type="organization",
read_relation="can_list_org",
update_relation="can_manage_settings",
delete_relation="can_manage_settings"
)
def perform_create(self, serializer):
raw_user_id = self.request.rebac_user.replace("user:", "")
serializer.save(creator_id=raw_user_id)
class FolderViewSet(viewsets.ModelViewSet):
queryset = Folder.objects.all()
serializer_class = FolderSerializer
permission_classes = [IsRebacAuthorized]
# 🛡️ Handles Object-Level AND Parent Cascading
rebac_config = RebacViewConfig(
object_type="folder",
read_relation="can_list_folder",
update_relation="can_edit_folder",
delete_relation="can_edit_folder",
create_scope_type="organization",
create_scope_field="organization_id",
create_relation="can_manage_settings"
)
def perform_create(self, serializer):
raw_user_id = self.request.rebac_user.replace("user:", "")
serializer.save(creator_id=raw_user_id)
class DocumentViewSet(viewsets.ModelViewSet):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
permission_classes = [IsRebacAuthorized]
# 🛡️ Handles Object-Level AND Parent Cascading
rebac_config = RebacViewConfig(
object_type="document",
read_relation="can_read_document",
update_relation="can_update",
delete_relation="can_delete",
create_scope_type="folder",
create_scope_field="folder_id",
create_relation="can_add_items"
)
def perform_create(self, serializer):
raw_user_id = self.request.rebac_user.replace("user:", "")
serializer.save(creator_id=raw_user_id)
How the Magic Happens
If a user tries to send a POST /api/documents/ payload with {"folder_id": "999", "title": "Secret Doc"}, the IsRebacAuthorized permission class will automatically intercept the request. It will query ReBAC: "Does user:123 have the can_add_items permission on folder:999?"
If ReBAC says no, the view instantly returns a 403 Forbidden without a single line of business logic running in your ViewSet! If ReBAC says yes, the record saves, and your model automatically fires the new role tuples into the Outbox table for Celery to sync. Clean Architecture at its finest!
Method C: The Unified RebacViewMixin
While the IsRebacAuthorized permission class is fantastic for explicit authorization boundary checks, the RebacViewMixin offers a complete, unified approach for views that also need automatic database filtering.
By defining your RebacViewConfig, the mixin handles three massive DRF lifecycle hooks automatically: Queryset Filtering for lists, Parent Cascading for creation, and HTTP method mapping for object details.
When you define a custom action, DRF sets self.action = "archive". Then, when you call self.get_object(), DRF automatically triggers self.check_object_permissions(). Our RebacViewMixin intercepts this, looks up the action name in your action_relations dictionary, and enforces the "can_archive" rule—all before your business logic even runs!
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rebac.mixins import RebacViewMixin
from rebac.structs import RebacViewConfig
from .models import Document
from .serializers import DocumentSerializer
from .services import DocumentService
class DocumentViewSet(RebacViewMixin, viewsets.ModelViewSet):
"""
A unified ViewSet secured entirely by the RebacViewMixin.
No business logic or permission parsing lives in this class!
"""
queryset = Document.objects.all()
serializer_class = DocumentSerializer
# The single source of truth for view-level authorization!
rebac_config = RebacViewConfig(
object_type="document",
read_relation="can_read_document",
update_relation="can_update",
delete_relation="can_delete",
create_scope_type="folder",
create_scope_field="folder_id",
create_relation="can_add_items",
action_relations={
"archive": "can_archive"
}
)
def perform_create(self, serializer):
# The mixin already verified we have 'can_add_items' on the parent folder!
# Now we delegate to our Service layer (Layer 2) to handle business logic.
raw_user_id = self.request.rebac_user.replace("user:", "")
service = DocumentService()
service.create_document(
data=serializer.validated_data,
creator_id=raw_user_id
)
@action(detail=True, methods=['post'])
def archive(self, request, pk=None):
"""
Custom endpoint to archive a document.
Route: POST /documents/{id}/archive/
"""
# 1. Fetch the object.
# CRITICAL: This automatically triggers check_object_permissions().
# The RebacViewMixin will see self.action == "archive", map it to "can_archive",
# and query ReBAC. If denied, it raises 403 Forbidden instantly.
document = self.get_object()
# 2. Delegate the actual business logic to the Service Layer
service = DocumentService()
service.archive_document(document=document)
# 3. Return standard REST response
return Response(
{"status": "Document successfully archived."},
status=status.HTTP_200_OK
)
Architectural Notes
@action(detail=True): It is crucial thatdetail=Trueis set. This tells DRF that this endpoint operates on a specific instance (which requires an ID in the URL). This is what allowsself.get_object()to fetch the specific document so the mixin can check its ReBAC relation.- Method constraints: By restricting
methods=['post'], we ensure that state-changing operations aren't accidentally triggered viaGETrequests, adhering strictly to RESTful best practices.
How the Mixin Hooks Work Under the Hood
The RebacViewMixin overrides three core DRF methods to apply your RebacViewConfig dataclass safely:
- Hook 1: Listing (
get_queryset) If the request is for a list (meaning no lookup kwarg is present), the mixin extracts theread_relation. It reaches out to ReBAC, fetches an array of allowed IDs using the injectedrequest.rebac_user, and applies an.id__infilter to your standard Django queryset. - Hook 2: Creation (
check_permissions) If the request method isPOST, the mixin extracts thecreate_parentconfiguration. It intercepts the incoming JSON payload, grabs the UUID from your specifiedcreate_scope_field(e.g.,folder_id), and asks ReBAC if the user holds the required relation on that parent object. If not, it instantly raises aPermissionDeniedexception. - Hook 3: Mutation/Detail (
check_object_permissions) When a single object is requested (e.g., for an update or delete), the mixin checks theupdate_relationordelete_relationagainst the currentrequest.method(oraction_relationsif a custom ViewSet action is used). It performs a preciseClientCheckRequestto ensure the user has the mapped permission on that exact object instance.