User-Based Access Control
Implement fine-grained user-based access control for personal data, documents, and resources.
User-Owned Resources
The most common pattern is resources owned by individual users:
Simple User Ownership
from django_rls.models import RLSModel
from django_rls.policies import UserPolicy
class PersonalNote(RLSModel):
title = models.CharField(max_length=200)
content = 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 UserProfile(RLSModel):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField()
avatar = models.ImageField(upload_to='avatars/')
is_public = models.BooleanField(default=False)
class Meta:
rls_policies = [
# Users can always see their own profile
UserPolicy('owner_policy', user_field='user'),
# Others can see if public
CustomPolicy(
'public_policy',
expression='is_public = true'
),
]
Collaborative Features
Shared Documents
class Document(RLSModel):
title = models.CharField(max_length=200)
content = models.TextField()
owner = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
rls_policies = [
UserPolicy('owner_policy', user_field='owner'),
]
class DocumentShare(RLSModel):
document = models.ForeignKey(Document, on_delete=models.CASCADE)
shared_with = models.ForeignKey(User, on_delete=models.CASCADE)
permission = models.CharField(max_length=10, choices=[
('view', 'View'),
('edit', 'Edit'),
])
shared_at = models.DateTimeField(auto_now_add=True)
class Meta:
rls_policies = [
# Document owner can manage shares
CustomPolicy(
'owner_can_manage',
expression="""
document_id IN (
SELECT id FROM myapp_document
WHERE owner_id = current_setting('rls.user_id')::integer
)
"""
),
# Shared users can see their shares
UserPolicy('shared_user', user_field='shared_with'),
]
# Update Document model to include sharing
class Document(RLSModel):
# ... fields as above
class Meta:
rls_policies = [
# Owner access
UserPolicy('owner_policy', user_field='owner'),
# Shared access
CustomPolicy(
'shared_policy',
expression="""
id IN (
SELECT document_id FROM myapp_documentshare
WHERE shared_with_id = current_setting('rls.user_id')::integer
)
"""
),
]
Team Collaboration
class Team(models.Model):
name = models.CharField(max_length=100)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
class TeamMember(models.Model):
team = models.ForeignKey(Team, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=[
('owner', 'Owner'),
('admin', 'Admin'),
('member', 'Member'),
])
class TeamResource(RLSModel):
team = models.ForeignKey(Team, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
rls_policies = [
CustomPolicy(
'team_member_access',
expression="""
team_id IN (
SELECT team_id FROM myapp_teammember
WHERE user_id = current_setting('rls.user_id')::integer
)
"""
),
]
Hierarchical Access
Manager-Employee Structure
class Employee(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
manager = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='direct_reports'
)
class PerformanceReview(RLSModel):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
reviewer = models.ForeignKey(Employee, on_delete=models.CASCADE,
related_name='reviews_given')
content = models.TextField()
rating = models.IntegerField()
class Meta:
rls_policies = [
CustomPolicy(
'employee_and_manager_access',
expression="""
-- Employee can see their own reviews
employee_id IN (
SELECT id FROM myapp_employee
WHERE user_id = current_setting('rls.user_id')::integer
)
OR
-- Manager can see their reports' reviews
employee_id IN (
SELECT id FROM myapp_employee
WHERE manager_id IN (
SELECT id FROM myapp_employee
WHERE user_id = current_setting('rls.user_id')::integer
)
)
OR
-- Reviewer can see reviews they wrote
reviewer_id IN (
SELECT id FROM myapp_employee
WHERE user_id = current_setting('rls.user_id')::integer
)
"""
),
]
Privacy Controls
Granular Privacy Settings
class UserContent(RLSModel):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
content = models.TextField()
visibility = models.CharField(max_length=20, choices=[
('private', 'Private'),
('friends', 'Friends Only'),
('public', 'Public'),
])
class Meta:
rls_policies = [
CustomPolicy(
'visibility_policy',
expression="""
-- Owner always has access
owner_id = current_setting('rls.user_id')::integer
OR
-- Public content
visibility = 'public'
OR
-- Friends only
(
visibility = 'friends'
AND owner_id IN (
SELECT friend_id FROM myapp_friendship
WHERE user_id = current_setting('rls.user_id')::integer
AND status = 'accepted'
)
)
"""
),
]
class Friendship(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
friend = models.ForeignKey(User, on_delete=models.CASCADE,
related_name='friend_requests')
status = models.CharField(max_length=20, choices=[
('pending', 'Pending'),
('accepted', 'Accepted'),
('blocked', 'Blocked'),
])
Admin Override
Sometimes admins need to bypass RLS:
class SupportTicket(RLSModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
subject = models.CharField(max_length=200)
description = models.TextField()
status = models.CharField(max_length=20)
assigned_to = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL,
related_name='assigned_tickets'
)
class Meta:
rls_policies = [
# User can see their own tickets
UserPolicy('user_policy', user_field='user'),
# Assigned support staff can see tickets
UserPolicy('assigned_policy', user_field='assigned_to'),
# Admins can see all tickets
CustomPolicy(
'admin_policy',
expression="""
EXISTS (
SELECT 1 FROM auth_user
WHERE id = current_setting('rls.user_id')::integer
AND is_staff = true
)
"""
),
]
Implementation Tips
1. User Context Helper
from django_rls.db.functions import set_rls_context
class UserContextMixin:
"""Mixin for views that need user context."""
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
# Set additional user context
set_rls_context('user_role', request.user.profile.role)
set_rls_context('user_department', request.user.profile.department)
return super().dispatch(request, *args, **kwargs)
2. Bulk Operations
@login_required
def bulk_update_documents(request):
# This will only update documents the user owns
updated = Document.objects.filter(
id__in=request.POST.getlist('document_ids')
).update(
updated_at=timezone.now(),
status='reviewed'
)
# 'updated' contains only the count of documents actually updated
messages.success(request, f'Updated {updated} documents')
3. Performance Optimization
class OptimizedDocument(RLSModel):
owner = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True)
# ... other fields
class Meta:
rls_policies = [
UserPolicy('owner_policy', user_field='owner'),
]
indexes = [
models.Index(fields=['owner', '-created_at']),
models.Index(fields=['owner', 'status']),
]
Testing User-Based Access
class UserAccessTest(TestCase):
def test_user_isolation(self):
user1 = User.objects.create_user('user1')
user2 = User.objects.create_user('user2')
# Create documents
set_rls_context('user_id', user1.id)
doc1 = Document.objects.create(
title='User 1 Doc',
owner=user1
)
set_rls_context('user_id', user2.id)
doc2 = Document.objects.create(
title='User 2 Doc',
owner=user2
)
# Test isolation
set_rls_context('user_id', user1.id)
visible_docs = Document.objects.all()
self.assertEqual(visible_docs.count(), 1)
self.assertEqual(visible_docs.first(), doc1)