This document provides a comprehensive guide to understanding and using Row-Level Security (RLS) in the FastAPI template project.
- Overview
- Key Concepts
- Configuration
- Model Development
- API Usage
- Admin Operations
- Troubleshooting
- Best Practices
Row-Level Security (RLS) provides automatic data isolation at the database level, ensuring that users can only access data they own. This is implemented using PostgreSQL's Row-Level Security feature with automatic policy generation and enforcement.
- Automatic Data Isolation: Users can only see their own data without explicit filtering
- Database-Level Security: Security is enforced at the database layer, not just application layer
- Minimal Developer Overhead: RLS is automatically applied to models that inherit from
UserScopedBase - Admin Bypass: Admins can access all data when needed for maintenance operations
Models that inherit from UserScopedBase automatically get:
- An
owner_idfield with foreign key touser.id - Automatic registration for RLS policy generation
- RLS policies applied during database migrations
- User isolation enforcement at the database level
from app.core.rls import UserScopedBase
class MyModel(UserScopedBase, table=True):
__tablename__ = "my_model"
id: UUID = Field(default_factory=uuid4, primary_key=True)
title: str = Field(max_length=255)
# owner_id is automatically inherited from UserScopedBaseRLS policies are automatically generated for each UserScopedBase model:
- SELECT: Users can only see records where
owner_idmatches their user ID - INSERT: Users can only insert records with their own
owner_id - UPDATE: Users can only update records they own
- DELETE: Users can only delete records they own
Admin users can bypass RLS policies through:
- User-Level Admin: Regular users with
is_superuser=True - Database-Level Admin: Dedicated database roles for maintenance operations
# Enable/disable RLS
RLS_ENABLED=true
# Force RLS even for privileged roles
RLS_FORCE=false
# Database roles for RLS
RLS_APP_USER=rls_app_user
RLS_APP_PASSWORD=changethis
RLS_MAINTENANCE_ADMIN=rls_maintenance_admin
RLS_MAINTENANCE_ADMIN_PASSWORD=changethis
# Initial users for RLS demonstration
FIRST_USER=user@example.com
FIRST_USER_PASSWORD=changethis
FIRST_SUPERUSER=admin@example.com
FIRST_SUPERUSER_PASSWORD=changethisRLS configuration is managed in app/core/config.py:
class Settings(BaseSettings):
RLS_ENABLED: bool = True
RLS_FORCE: bool = False
# Database role configuration
RLS_APP_USER: str = "rls_app_user"
RLS_APP_PASSWORD: str = "changethis"
RLS_MAINTENANCE_ADMIN: str = "rls_maintenance_admin"
RLS_MAINTENANCE_ADMIN_PASSWORD: str = "changethis"To create a model with RLS enforcement:
- Inherit from UserScopedBase:
from app.core.rls import UserScopedBase
class Task(UserScopedBase, table=True):
__tablename__ = "task"
id: UUID = Field(default_factory=uuid4, primary_key=True)
title: str = Field(max_length=255)
description: Optional[str] = None
# owner_id is automatically inherited- Define Relationships:
class Task(UserScopedBase, table=True):
# ... fields ...
owner: User = Relationship(back_populates="tasks")- Update User Model:
class User(UserBase, table=True):
# ... fields ...
tasks: List[Task] = Relationship(back_populates="owner", cascade_delete=True)Models that inherit from UserScopedBase are automatically registered for RLS policy generation. This happens when the model class is defined, so no additional registration is required.
Regular users automatically have RLS context set through FastAPI dependencies:
from app.api.deps import RLSSessionDep, CurrentUser
@router.get("/items/")
def read_items(session: RLSSessionDep, current_user: CurrentUser):
# RLS context is automatically set
# User can only see their own items
items = session.exec(select(Item)).all()
return itemsUse the RLS-compatible CRUD operations:
from app import crud
# Create item (automatically sets owner_id)
item = crud.create_item(session=session, item_in=item_data, owner_id=user.id)
# Get user's items (RLS enforced)
items = crud.get_items(session=session, owner_id=user.id)
# Update item (ownership verified)
item = crud.update_item(session=session, db_item=item, item_in=update_data, owner_id=user.id)
# Delete item (ownership verified)
crud.delete_item(session=session, item_id=item_id, owner_id=user.id)Admin users can access all data through RLS policies:
from app.api.deps import AdminSessionDep
@router.get("/admin/items/")
def read_all_items(session: AdminSessionDep, current_user: CurrentUser):
# Admin can see all items regardless of ownership
items = session.exec(select(Item)).all()
return itemsUse admin CRUD operations for maintenance:
# Get any item (admin only)
item = crud.get_item_admin(session=session, item_id=item_id)
# Update any item (admin only)
item = crud.update_item_admin(session=session, db_item=item, item_in=update_data)
# Delete any item (admin only)
crud.delete_item_admin(session=session, item_id=item_id)For programmatic admin access:
from app.core.rls import AdminContext
with AdminContext.create_full_admin(user_id, session) as admin_ctx:
# All operations in this block run with admin privileges
items = session.exec(select(Item)).all()Symptoms: Users can see all data instead of just their own.
Solutions:
- Check that
RLS_ENABLED=truein environment variables - Verify that models inherit from
UserScopedBase - Run database migrations:
alembic upgrade head - Check RLS policies in database:
SELECT * FROM pg_policies WHERE tablename = 'your_table';
Symptoms: Users get 403 errors when accessing their own data.
Solutions:
- Verify RLS context is set:
SELECT current_setting('app.user_id'); - Check user authentication and token validity
- Ensure proper session context management in API endpoints
Symptoms: Admin users cannot access all data.
Solutions:
- Verify user has
is_superuser=True - Check admin session dependency usage
- Verify RLS policies allow admin access
-- Check if RLS is enabled on a table
SELECT relrowsecurity FROM pg_class WHERE relname = 'item';
-- List all RLS policies
SELECT schemaname, tablename, policyname, cmd, qual
FROM pg_policies
WHERE tablename = 'item';
-- Check current session context
SELECT current_setting('app.user_id'), current_setting('app.role');
-- Test RLS policies
SET app.user_id = 'user-uuid-here';
SET app.role = 'user';
SELECT * FROM item; -- Should only show user's itemsEnable debug logging to troubleshoot RLS issues:
import logging
logging.getLogger('app.core.rls').setLevel(logging.DEBUG)- Always inherit from UserScopedBase for user-owned data
- Use proper relationships between User and RLS-scoped models
- Index the owner_id field (automatically done by UserScopedBase)
- Consider cascade delete for related data cleanup
- Use RLSSessionDep for user endpoints
- Use AdminSessionDep for admin endpoints
- Implement proper error handling for RLS violations
- Provide clear error messages for access denied scenarios
- Never bypass RLS in regular user operations
- Use admin context sparingly and only when necessary
- Audit admin operations for security compliance
- Test RLS policies with different user scenarios
- Monitor RLS performance impact on queries
- Use appropriate indexes on owner_id fields
- Consider query optimization for large datasets
- Test concurrent user scenarios for performance validation
- Always run migrations after model changes
- Test RLS policies in development environment
- Verify policy application after migrations
- Document any manual policy changes
See RLS Examples for detailed code examples and use cases.
For additional help with RLS implementation:
- Check the Troubleshooting Guide
- Review the Performance Tests
- Consult the API Documentation
- Check the Database ERD for model relationships