Skip to main content

Basic Usage

Let's build a simple task management application with RLS to understand the basics.

The Problem

In a typical Django application, you need to remember to filter querysets:

# Without RLS - Easy to forget filtering!
def task_list(request):
# WRONG: Shows all tasks to everyone
tasks = Task.objects.all()

# RIGHT: Must remember to filter
tasks = Task.objects.filter(owner=request.user)
return render(request, 'tasks.html', {'tasks': tasks})

The RLS Solution

With Django RLS, filtering happens automatically at the database level:

1. Define Your Model

from django.db import models
from django.contrib.auth.models import User
from django_rls.models import RLSModel
from django_rls.policies import UserPolicy

class Task(RLSModel):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
completed = models.BooleanField(default=False)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ['-created_at']
rls_policies = [
UserPolicy('owner_policy', user_field='owner'),
]

def __str__(self):
return self.title

2. Create Views

Your views become simpler and more secure:

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .models import Task
from .forms import TaskForm

@login_required
def task_list(request):
# Automatically filtered to current user's tasks!
tasks = Task.objects.all()
return render(request, 'tasks/list.html', {'tasks': tasks})

@login_required
def task_create(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
task = form.save(commit=False)
task.owner = request.user # Set owner
task.save()
return redirect('task_list')
else:
form = TaskForm()
return render(request, 'tasks/form.html', {'form': form})

@login_required
def task_update(request, pk):
# Will only find tasks owned by current user
task = get_object_or_404(Task, pk=pk)
# ... rest of view

3. Enable RLS

After creating your models, enable RLS:

python manage.py makemigrations
python manage.py migrate
python manage.py enable_rls

What's Happening Behind the Scenes

When RLS is enabled, PostgreSQL automatically adds WHERE clauses:

-- What you write:
SELECT * FROM myapp_task;

-- What PostgreSQL executes for user_id=1:
SELECT * FROM myapp_task
WHERE owner_id = current_setting('rls.user_id')::integer;

Complete Example Application

Models (models.py)

from django.db import models
from django.contrib.auth.models import User
from django_rls.models import RLSModel
from django_rls.policies import UserPolicy

class Project(RLSModel):
name = models.CharField(max_length=100)
description = models.TextField()
owner = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
rls_policies = [
UserPolicy('owner_policy', user_field='owner'),
]

class Task(RLSModel):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
assigned_to = models.ForeignKey(User, on_delete=models.CASCADE)
completed = models.BooleanField(default=False)
due_date = models.DateField(null=True, blank=True)

class Meta:
rls_policies = [
UserPolicy('assigned_policy', user_field='assigned_to'),
]

Views (views.py)

from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.db.models import Count
from .models import Project, Task

@login_required
def dashboard(request):
# All queries automatically filtered!
context = {
'projects': Project.objects.all(),
'tasks': Task.objects.filter(completed=False),
'completed_tasks': Task.objects.filter(completed=True).count(),
'project_stats': Project.objects.annotate(
task_count=Count('task')
),
}
return render(request, 'dashboard.html', context)

API Views (api.py)

from rest_framework import viewsets
from .models import Task, Project
from .serializers import TaskSerializer, ProjectSerializer

class ProjectViewSet(viewsets.ModelViewSet):
# No need for get_queryset() filtering!
queryset = Project.objects.all()
serializer_class = ProjectSerializer

class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer

def perform_create(self, serializer):
serializer.save(assigned_to=self.request.user)

Common Patterns

Shared Access

Allow multiple users to access the same records:

class SharedDocument(RLSModel):
title = models.CharField(max_length=200)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
shared_with = models.ManyToManyField(User, related_name='shared_docs')

class Meta:
rls_policies = [
# Owner can access
UserPolicy('owner_policy', user_field='owner'),
# Shared users can access
CustomPolicy(
'shared_policy',
expression="""
id IN (
SELECT shareddocument_id
FROM myapp_shareddocument_shared_with
WHERE user_id = current_setting('rls.user_id')::integer
)
"""
),
]

Hierarchical Access

Managers can see their team's data:

class TeamTask(RLSModel):
title = models.CharField(max_length=200)
assigned_to = models.ForeignKey(User, on_delete=models.CASCADE)

class Meta:
rls_policies = [
CustomPolicy(
'team_policy',
expression="""
assigned_to_id = current_setting('rls.user_id')::integer
OR assigned_to_id IN (
SELECT id FROM auth_user
WHERE manager_id = current_setting('rls.user_id')::integer
)
"""
),
]

Time-Based Access

Records accessible only during certain periods:

class TimedContent(RLSModel):
title = models.CharField(max_length=200)
available_from = models.DateTimeField()
available_until = models.DateTimeField()

class Meta:
rls_policies = [
CustomPolicy(
'time_policy',
expression="""
CURRENT_TIMESTAMP BETWEEN available_from AND available_until
"""
),
]

Benefits

  1. Security by Default: Can't accidentally expose data
  2. Simpler Code: No need to filter in every view
  3. Consistency: Same rules everywhere (views, API, admin)
  4. Performance: Database-optimized filtering
  5. Auditability: Security rules in one place