Nested Hierarchies (Tree Views)
When building nested API responses (e.g., Organization -> Folders -> Documents), standard nested DRF serializers can easily trigger massive N+1 queries or accidentally leak unauthorized records.
To solve this, use a 3-step Prefetch Pattern:
- Query ReBAC: Call
list_objectsfor each level of the hierarchy (e.g., allowed Orgs, allowed Folders, allowed Documents). - Filter Base Querysets: Create Django querysets using
.filter(id__in=allowed_ids)for each level. - Stitch with Prefetch: Use Django's
Prefetch('related_name', queryset=filtered_queryset)to stitch the objects together.
Example 1: Secure Tree API View (Raw)
Assume you have a standard nested serializer setup where an Organization has many folders, and a Folder has many documents.
Here is how you write a single APIView that returns the entire tree securely for the current user:
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db.models import Prefetch
from rebac.conf import get_setting
from rebac.utils import get_rebac_client
from .models import Organization, Folder, Document
from .serializers import OrganizationNestedSerializer
class SecureHierarchyTreeAPIView(APIView):
"""
Returns a deeply nested tree of Orgs -> Folders -> Documents.
100% ReBAC-secured at every single level, using only 3 DB queries.
"""
def get_rebac_ids(self, rebac_user: str, object_type: str, relation: str) -> list[str]:
"""Helper to fetch allowed IDs for a specific type from ReBAC."""
client = get_rebac_client()
# and automatically strips the prefixes for you.
allowed_ids = client.list_objects(
user=rebac_user,
relation=relation,
object_type=object_type
)
return allowed_ids
def get(self, request):
user_attr = get_setting("REBAC_USER_ATTR")
rebac_user = getattr(request, user_attr, None)
if not rebac_user:
return Response({"error": "Missing identity context."}, status=401)
# STEP 1: Query ReBAC for the allowed IDs (3 Fast Network Calls)
allowed_org_ids = self.get_rebac_ids(rebac_user, "organization", "can_list_org")
allowed_folder_ids = self.get_rebac_ids(rebac_user, "folder", "can_list_folder")
allowed_doc_ids = self.get_rebac_ids(rebac_user, "document", "can_read_document")
# STEP 2: Filter the base querysets securely
secure_docs = Document.objects.filter(id__in=allowed_doc_ids)
# STEP 3: Stitch the tree together from the bottom up using Prefetch
secure_folders = Folder.objects.filter(id__in=allowed_folder_ids).prefetch_related(
Prefetch('documents', queryset=secure_docs) # Stitches Docs into Folders
)
secure_orgs = Organization.objects.filter(id__in=allowed_org_ids).prefetch_related(
Prefetch('folders', queryset=secure_folders) # Stitches Folders into Orgs
)
# 4. Serialize the final, perfectly secured tree!
serializer = OrganizationNestedSerializer(secure_orgs, many=True)
return Response(serializer.data)
Example 2: Using DRF Generic Views (ListAPIView)
If you prefer using DRF's Generic Views to take advantage of built-in pagination, filtering, and standard DRF workflows, you can place the exact same Prefetch logic inside the get_queryset() method.
from rest_framework import generics
from rest_framework.exceptions import AuthenticationFailed
from django.db.models import Prefetch
from rebac.conf import get_setting
from rebac.utils import get_rebac_client
from .models import Organization, Folder, Document
from .serializers import OrganizationNestedSerializer
class SecureHierarchyTreeListAPIView(generics.ListAPIView):
"""
Returns a deeply nested, ReBAC-secured tree of Orgs -> Folders -> Documents.
Built on top of DRF's standard generic ListAPIView.
"""
# Standard DRF setup
serializer_class = OrganizationNestedSerializer
def get_rebac_ids(self, rebac_user: str, object_type: str, relation: str) -> list[str]:
"""Helper to fetch allowed IDs for a specific type from ReBAC Backend."""
client = get_rebac_client()
# and automatically strips the prefixes for you.
allowed_ids = client.list_objects(
user=rebac_user,
relation=relation,
object_type=object_type
)
return allowed_ids
def get_queryset(self):
"""
Intercepts the queryset building process to inject ReBAC security
and high-performance database Prefetching.
"""
user_attr = get_setting("REBAC_USER_ATTR")
rebac_user = getattr(self.request, user_attr, None)
if not rebac_user:
raise AuthenticationFailed("Missing identity context.")
# STEP 1: Query ReBAC for the allowed IDs
allowed_org_ids = self.get_rebac_ids(rebac_user, "organization", "can_list_org")
allowed_folder_ids = self.get_rebac_ids(rebac_user, "folder", "can_list_folder")
allowed_doc_ids = self.get_rebac_ids(rebac_user, "document", "can_read_document")
# STEP 2: Filter the base querysets securely
secure_docs = Document.objects.filter(id__in=allowed_doc_ids)
# STEP 3: Stitch the tree together from the bottom up using Prefetch
secure_folders = Folder.objects.filter(id__in=allowed_folder_ids).prefetch_related(
Prefetch('documents', queryset=secure_docs) # Stitches Docs into Folders
)
# STEP 4: Return the finalized, top-level queryset to DRF
return Organization.objects.filter(id__in=allowed_org_ids).prefetch_related(
Prefetch('folders', queryset=secure_folders) # Stitches Folders into Orgs
)
Serializer for the Standard Views
This is the standard DocumentSerializer referenced in the DocumentCreateAPIView and DocumentViewSet examples in Securing API Views. Notice how creator_id is set to read_only=True because our view logic handles injecting the current user's ID during creation.
# serializers.py
from rest_framework import serializers
from .models import Organization, Folder, Document
class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ['id', 'title', 'creator_id', 'created_at']
class FolderNestedSerializer(serializers.ModelSerializer):
# This matches the related_name we used in the Prefetch object!
documents = DocumentSerializer(many=True, read_only=True)
class Meta:
model = Folder
fields = ['id', 'name', 'documents']
class OrganizationNestedSerializer(serializers.ModelSerializer):
# This matches the related_name we used in the Prefetch object!
folders = FolderNestedSerializer(many=True, read_only=True)
class Meta:
model = Organization
fields = ['id', 'name', 'folders']
This ensures you execute only a few fast network calls and a few fast database queries, returning a 100% secure JSON tree.
The ReBAC Schema Powering This View
For the Python code above to work flawlessly, your ReBAC schema must follow the Roles vs. Permissions pattern.
Notice how the generic views do not check if the user is an admin or an editor. They strictly check the Permissions (can_list_org, can_list_folder, can_read_document), allowing the ReBAC graph to calculate all the complex role inheritance automatically.
👉 See the Schema Design Guide for the exact ReBAC Schema required to power this view.