Skip to main content
Version: 2.1

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.