Middleware
The RLSContextMiddleware is responsible for setting PostgreSQL session variables that your RLS policies use for filtering.
How It Works
- Extracts user and tenant information from the request
- Sets PostgreSQL session variables using
SET LOCAL
- These variables are available to RLS policies during the request
- Clears the context after the request completes
Basic Setup
Add the middleware after Django's authentication middleware:
MIDDLEWARE = [
# ... other middleware
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_rls.middleware.RLSContextMiddleware', # Must be after auth
# ... other middleware
]
Default Behavior
By default, the middleware sets:
rls.user_id
: The ID of the authenticated userrls.tenant_id
: Extracted from the request (if available)
Customizing Context Extraction
Custom Tenant Detection
The middleware looks for tenant information in this order:
request.tenant
(set by other middleware)request.user.profile.tenant_id
request.session['tenant_id']
You can customize this by subclassing:
from django_rls.middleware import RLSContextMiddleware
class CustomRLSMiddleware(RLSContextMiddleware):
def _get_tenant_id(self, request):
# Custom logic to extract tenant
if hasattr(request, 'organization'):
return request.organization.id
# From subdomain
host = request.get_host()
subdomain = host.split('.')[0]
try:
from myapp.models import Tenant
tenant = Tenant.objects.get(subdomain=subdomain)
return tenant.id
except Tenant.DoesNotExist:
return None
Adding Custom Context Variables
from django_rls.middleware import RLSContextMiddleware
from django_rls.db.functions import set_rls_context
class ExtendedRLSMiddleware(RLSContextMiddleware):
def _set_rls_context(self, request):
# Call parent to set user_id and tenant_id
super()._set_rls_context(request)
# Add custom context
if hasattr(request.user, 'profile'):
profile = request.user.profile
set_rls_context('department', profile.department)
set_rls_context('role', profile.role)
set_rls_context('region', profile.region)
Context Variables in Policies
Use the context variables in your policies:
class RegionalData(RLSModel):
region = models.CharField(max_length=50)
class Meta:
rls_policies = [
CustomPolicy(
'region_policy',
expression="region = current_setting('rls.region', true)"
),
]
Handling Anonymous Users
The middleware handles anonymous users gracefully:
class PublicRLSMiddleware(RLSContextMiddleware):
def _set_rls_context(self, request):
if request.user.is_authenticated:
super()._set_rls_context(request)
else:
# Set a special context for anonymous users
set_rls_context('user_id', '-1')
set_rls_context('is_anonymous', 'true')
Performance Considerations
- Minimize Context Calls: Set all context variables at once
- Cache Lookups: Cache tenant lookups if they're expensive
- Use Connection Pooling: RLS context is per-connection
Example with caching:
from django.core.cache import cache
class CachedTenantMiddleware(RLSContextMiddleware):
def _get_tenant_id(self, request):
if not request.user.is_authenticated:
return None
cache_key = f'user_tenant_{request.user.id}'
tenant_id = cache.get(cache_key)
if tenant_id is None:
tenant_id = request.user.profile.tenant_id
cache.set(cache_key, tenant_id, 300) # Cache for 5 minutes
return tenant_id
Testing with Middleware
When testing, you can set context manually:
from django.test import TestCase
from django_rls.db.functions import set_rls_context
class MyTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user('testuser')
self.tenant = Tenant.objects.create(name='Test Tenant')
def test_with_context(self):
# Manually set context for tests
set_rls_context('user_id', self.user.id)
set_rls_context('tenant_id', self.tenant.id)
# Your test code here
documents = Document.objects.all()
# ...
Debugging Middleware
Enable debug logging to see what context is being set:
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django_rls': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}
Common Patterns
Multi-Database Setup
class MultiDBRLSMiddleware(RLSContextMiddleware):
def _set_rls_context(self, request):
# Set context for each database
for db_alias in ['default', 'tenant_db']:
with connections[db_alias].cursor() as cursor:
cursor.execute(
"SELECT set_config('rls.user_id', %s, true)",
[str(request.user.id)]
)
API Authentication
class APIRLSMiddleware(RLSContextMiddleware):
def _set_rls_context(self, request):
# Handle both session and token auth
if hasattr(request, 'auth'):
# DRF token authentication
set_rls_context('user_id', request.user.id)
set_rls_context('api_key', request.auth.key)
else:
# Regular session auth
super()._set_rls_context(request)