2. Basic Departmental Isolation
Our first requirement is Strict Departmental Isolation.
A "Sales" employee must never see "Engineering" documents.
This seems simple, but it is the foundation of multi-tenant security.
The Approach: Without django-rls
In a traditional Django application, you are responsible for enforcing this rule in every single place data is accessed.
1. The View Layer
You must remember to filter every QuerySet.
# views.py (The Manual Way)
def document_list(request):
# DANGER: If you type .all() here, you just leaked data!
# docs = ERPDocument.objects.all() <-- WRONG
# You must always filter:
user_dept = request.user.profile.department
docs = ERPDocument.objects.filter(department=user_dept)
return render(request, 'list.html', {'docs': docs})
2. The API Layer
If you use Django REST Framework, you must override get_queryset on every ViewSet.
# api.py (The Manual Way)
class DocumentViewSet(viewsets.ModelViewSet):
def get_queryset(self):
# Again, easy to forget or get wrong
return ERPDocument.objects.filter(department=self.request.user.profile.department)
3. The Admin Panel
Often overlooked! By default, the Django Admin matches queryset.all(). This means any staff member can see everything. You have to override get_queryset in admin.py too.
The Risk
If you have 50 views and you forget the filter in just one, you have a security breach.
The Approach: With django-rls
With Row-Level Security, we move this logic into the database. We define it once, on the model, and it applies everywhere automatically.
1. Setting the Context
First, we need to ensure the database knows which department the current user belongs to. We use django-rls Context Middleware.
# myapp/middleware.py
from django_rls.db.functions import set_rls_context
class MegaCorpMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
set_rls_context('user_id', request.user.id)
# Set Department ID as 'tenant_id'
try:
dept_id = request.user.userprofile.department_id
set_rls_context('tenant_id', dept_id)
except AttributeError:
pass
return self.get_response(request)
2. Defining the Policy
Now we add the policy to ERPDocument.
# myapp/models.py
from django.db.models import Q
from django_rls.policies import ModelPolicy, RLS
class ERPDocument(RLSModel):
# ... fields ...
class Meta:
rls_policies = [
# Basic Isolation: Document Dept == User Dept
ModelPolicy(
'department_isolation',
filters=Q(department=RLS.tenant_id())
)
]
3. The Result
Now, your view becomes:
# views.py (The RLS Way)
def document_list(request):
# SAFE! automatically filtered by the DB
docs = ERPDocument.objects.all()
return render(request, 'list.html', {'docs': docs})
Even if you write ERPDocument.objects.all() in the Django shell, API, or Admin, the database will silently rewrite it to:
SELECT * FROM myapp_erpdocument
WHERE department_id = current_setting('rls.tenant_id')::integer;
The data leakage vector is eliminated at the source.