Understanding how Django RLS works under the hood can help you make better decisions when implementing Row Level Security in your applications. Let’s explore the architecture and execution flow.
Architecture Overview
Django RLS sits between your Django application and PostgreSQL, managing the RLS policies and context:
Django Application
↓
Django RLS
↙ ↘
Models Middleware → PostgreSQL (with RLS policies)
↖ ↗
Context Management
The Four-Step Process
1. Define Models with RLS Policies
You start by defining your models with RLS policies:
from django_rls.models import RLSModel
from django_rls.policies import UserPolicy
class Document(RLSModel):
title = models.CharField(max_length=200)
content = models.TextField()
owner = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
rls_policies = [
UserPolicy('owner_policy', user_field='owner')
]
When you run migrations, Django RLS automatically creates PostgreSQL RLS policies based on your configuration.
2. Set Context via Middleware
Django RLS includes middleware that automatically sets the user context for each request:
MIDDLEWARE = [
'django_rls.middleware.RLSMiddleware',
]
This middleware:
- Extracts the current user from the request
- Sets PostgreSQL context variables (like
current_user_id) - Makes context available throughout the request lifecycle
3. Execute Queries with Automatic Filtering
When you execute a query, PostgreSQL applies the RLS policies:
# Your code (same for all users)
my_docs = Document.objects.all()
# Behind the scenes:
# User A sees only their documents
# User B sees only their documents
# PostgreSQL enforces this at the database level
PostgreSQL’s RLS engine intercepts the query and applies the policies automatically.
4. Return Filtered Results
Only rows that pass the RLS policy are returned:
Query: SELECT * FROM documents
↓
RLS Policy Check: WHERE owner_id = current_user_id()
↓
Filtered Results: Only user's documents returned
How Policies Are Evaluated
When you define a policy, Django RLS translates it to a PostgreSQL RLS policy:
# Django RLS definition
UserPolicy('owner_policy', user_field='owner')
# Creates PostgreSQL policy like:
CREATE POLICY owner_policy ON documents
USING (owner_id = current_user_id())
WITH CHECK (owner_id = current_user_id());
Context Management
Context is managed through PostgreSQL’s session settings:
# Django RLS Middleware does this automatically:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute(
"SELECT set_config('app.user_id', %s, false)",
[str(request.user.id)]
)
This sets a context variable that RLS policies can reference.
Multi-Tenant Example
Let’s trace through a multi-tenant scenario:
class Document(RLSModel):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
rls_policies = [
TenantPolicy('tenant_policy', tenant_field='tenant'),
UserPolicy('owner_policy', user_field='owner'),
]
Execution Flow:
- Request comes in from User A in Tenant X
- Middleware sets context:
app.tenant_id = X,app.user_id = A - Query executed:
Document.objects.all() - Tenant policy applied:
WHERE tenant_id = X - Owner policy applied:
WHERE owner_id = A - Results returned: Only documents in Tenant X owned by User A
Performance Considerations
Query Optimization
PostgreSQL optimizes queries knowing about RLS policies:
# This query:
Document.objects.filter(status='active').count()
# Is executed as:
SELECT COUNT(*) FROM documents
WHERE status = 'active'
AND owner_id = current_user_id() -- Added by RLS
PostgreSQL combines your filters with RLS conditions for optimal performance.
Index Usage
RLS policies work seamlessly with indexes:
-- Your index
CREATE INDEX idx_documents_status ON documents(status);
-- RLS filters efficiently use your indexes
-- Even though the policy adds conditions
Migration and Policy Management
When you change your models, Django RLS handles policy updates through migrations:
$ python manage.py makemigrations
$ python manage.py migrate
Django RLS automatically:
- Creates new policies when you add them
- Updates policies when you modify them
- Removes policies when you delete them
Testing
Django RLS provides utilities to test policies:
from django_rls.test_utils import RLSTestCase
class DocumentTestCase(RLSTestCase):
def test_user_isolation(self):
# Sets up user context automatically
doc = Document.objects.create(owner=self.user)
self.assertEqual(Document.objects.count(), 1) # Only their doc
Summary
Django RLS works by:
- Defining policies in your models
- Creating PostgreSQL RLS policies through migrations
- Setting user context via middleware for each request
- Filtering queries automatically at the database level
This approach keeps your application code clean while ensuring security at the database level, where it matters most.