Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 4 additions & 13 deletions .cursor/rules/specify-rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Auto-generated from all feature plans. Last updated: 2025-10-03

## Active Technologies
- Python 3.11+ + SQLModel, Mermaid, Git hooks, pre-commit framework (001-as-a-first)
- Python 3.11+, SQLModel, FastAPI + PostgreSQL, Alembic, SQLModel, FastAPI, psycopg (002-tenant-isolation-via)
- PostgreSQL with RLS policies (002-tenant-isolation-via)

## Project Structure
```
Expand All @@ -19,20 +21,9 @@ cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLO
Python 3.11+: Follow standard conventions

## Recent Changes
- 002-tenant-isolation-via: Added Python 3.11+, SQLModel, FastAPI + PostgreSQL, Alembic, SQLModel, FastAPI, psycopg
- 002-tenant-isolation-via: Added Python 3.11+, SQLModel, FastAPI + PostgreSQL, Alembic, SQLModel, FastAPI, psycopg
- 001-as-a-first: Added Python 3.11+ + SQLModel, Mermaid, Git hooks, pre-commit framework

<!-- MANUAL ADDITIONS START -->

## Python Environment Management

**CRITICAL: Always use `uv` for Python commands in the backend directory**

- ✅ **DO**: `cd backend && uv run pytest tests/...`
- ✅ **DO**: `cd backend && uv run python script.py`
- ✅ **DO**: `cd backend && uv run mypy .`
- ❌ **DON'T**: Use system Python directly (`python`, `pytest`, etc.)
- ❌ **DON'T**: Use `python -m pytest` without `uv run`

This ensures consistent dependency management and virtual environment usage across all Python operations.

<!-- MANUAL ADDITIONS END -->
15 changes: 15 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,24 @@ STACK_NAME=full-stack-fastapi-project
# Backend
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
SECRET_KEY=changethis

# RLS (Row-Level Security) Configuration
RLS_ENABLED=true
RLS_FORCE=false

RLS_APP_USER=rls_app_user
RLS_APP_PASSWORD=changethis

RLS_MAINTENANCE_ADMIN=rls_maintenance_admin
RLS_MAINTENANCE_ADMIN_PASSWORD=changethis

FIRST_SUPERUSER=admin@example.com
FIRST_SUPERUSER_PASSWORD=changethis

# Initial User Configuration for RLS Demonstration
FIRST_USER=user@example.com
FIRST_USER_PASSWORD=changethis

# Emails
SMTP_HOST=
SMTP_USER=
Expand Down
15 changes: 13 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,26 @@ repos:
files: ^frontend/
- id: erd-generation
name: ERD generation
entry: bash -c 'cd backend && python scripts/generate_erd.py --validate --verbose --force'
entry: bash -c 'cd backend && source .venv/bin/activate && DETERMINISTIC_ERD_GENERATION=1 python scripts/generate_erd.py --validate --verbose --force'
language: system
types: [python]
files: ^backend/app/models\.py$
stages: [pre-commit]
always_run: false
pass_filenames: false
require_serial: true
description: "Generate and validate ERD diagrams from SQLModel definitions (Mermaid format)"
- id: rls-validation
name: RLS model validation
entry: bash -c 'cd backend && source .venv/bin/activate && python scripts/lint_rls.py --verbose'
language: system
types: [python]
files: ^backend/app/.*\.py$
stages: [pre-commit]
always_run: false
pass_filenames: true
require_serial: false
description: "Generate and validate ERD diagrams from SQLModel definitions (Mermaid format)"
description: "Validate that user-owned models inherit from UserScopedBase for RLS enforcement"

ci:
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
Expand Down
54 changes: 54 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# FastAPI Project - Backend

## Features

This FastAPI backend template includes:

- **Row-Level Security (RLS)**: Automatic data isolation using PostgreSQL RLS policies
- **User Authentication**: JWT-based authentication with user management
- **Admin Operations**: Admin bypass functionality for maintenance operations
- **Automatic Migrations**: Alembic migrations with RLS policy generation
- **API Documentation**: Auto-generated OpenAPI/Swagger documentation
- **Testing Suite**: Comprehensive tests for RLS functionality and isolation

## Requirements

* [Docker](https://www.docker.com/).
Expand All @@ -9,6 +20,49 @@

Start the local development environment with Docker Compose following the guide in [../development.md](../development.md).

## Row-Level Security (RLS)

This project implements PostgreSQL Row-Level Security for automatic data isolation. Users can only access data they own, enforced at the database level.

### Quick Start with RLS

1. **Environment Setup**: Ensure RLS is enabled in your `.env` file:
```bash
RLS_ENABLED=true
RLS_APP_USER=rls_app_user
RLS_APP_PASSWORD=changethis
RLS_MAINTENANCE_ADMIN=rls_maintenance_admin
RLS_MAINTENANCE_ADMIN_PASSWORD=changethis
```

2. **Create RLS-Scoped Models**: Inherit from `UserScopedBase`:
```python
from app.core.rls import UserScopedBase

class MyModel(UserScopedBase, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
title: str
# owner_id is automatically inherited
```

3. **Use RLS-Aware API Endpoints**: Use the provided dependencies:
```python
from app.api.deps import RLSSessionDep, CurrentUser

@router.get("/items/")
def read_items(session: RLSSessionDep, current_user: CurrentUser):
# User can only see their own items
items = session.exec(select(Item)).all()
return items
```

### RLS Documentation

- **[User Guide](../docs/security/rls-user.md)**: Comprehensive RLS usage guide
- **[Troubleshooting](../docs/security/rls-troubleshooting.md)**: Common issues and solutions
- **[Examples](../docs/examples/rls-examples.md)**: Code examples and use cases
- **[Database ERD](../docs/database/erd.md)**: Schema with RLS annotations

## General Workflow

By default, the dependencies are managed with [uv](https://docs.astral.sh/uv/), go there and install it.
Expand Down
29 changes: 29 additions & 0 deletions backend/app/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from app.models import SQLModel # noqa
from app.core.config import settings # noqa
from app.core.rls import rls_registry, policy_generator # noqa

target_metadata = SQLModel.metadata

Expand Down Expand Up @@ -60,6 +61,8 @@ def run_migrations_online():
In this scenario we need to create an Engine
and associate a connection with the context.

RLS policies are automatically applied after migrations if RLS is enabled.

"""
configuration = config.get_section(config.config_ini_section)
configuration["sqlalchemy.url"] = get_url()
Expand All @@ -77,6 +80,32 @@ def run_migrations_online():
with context.begin_transaction():
context.run_migrations()

# Apply RLS policies after migrations if RLS is enabled
if settings.RLS_ENABLED:
apply_rls_policies(connection)


def apply_rls_policies(connection):
"""Apply RLS policies to all registered RLS-scoped tables."""
from sqlalchemy import text

# Get all registered RLS tables
registered_tables = rls_registry.get_registered_tables()

for table_name, metadata in registered_tables.items():
try:
# Generate and execute RLS setup SQL
rls_sql_statements = policy_generator.generate_complete_rls_setup_sql(table_name)

for sql_statement in rls_sql_statements:
connection.execute(text(sql_statement))

print(f"Applied RLS policies to table: {table_name}")

except Exception as e:
print(f"Warning: Failed to apply RLS policies to table {table_name}: {e}")
# Continue with other tables even if one fails


if context.is_offline_mode():
run_migrations_offline()
Expand Down
Loading
Loading