163 lines
5.0 KiB
Markdown
163 lines
5.0 KiB
Markdown
# Plan 4 — Security
|
|
|
|
**Items covered:** #1 (no authentication), #11 (no rate limiting), #14 (unbounded limit parameter)
|
|
|
|
---
|
|
|
|
## Context
|
|
|
|
You are working on a FastAPI backend for a baby monitor / sleep tracker app. The codebase lives under `app/`. This plan adds three layers of security enforcement. Do them in order — auth first, then rate limiting, then the parameter bound. After each change, re-read the modified files to verify correctness, and run the tests.
|
|
|
|
---
|
|
|
|
## Tasks
|
|
|
|
### 1. Add API key authentication to all endpoints (`app/`)
|
|
|
|
#### Step 1 — Read the relevant files first
|
|
|
|
Read `app/config.py`, `app/main.py`, and all router files under `app/routers/` before making any changes. Understand how the app is structured and how dependencies are currently shared.
|
|
|
|
#### Step 2 — Add `API_KEY` to settings
|
|
|
|
In `app/config.py`, add an `API_KEY` field to the `Settings` model:
|
|
|
|
```python
|
|
API_KEY: str = ""
|
|
```
|
|
|
|
The empty default means auth is effectively disabled unless the env var is set. This allows existing tests to keep working without modification (see Step 5 for the alternative if you prefer strict-by-default).
|
|
|
|
#### Step 3 — Create the auth dependency
|
|
|
|
Create a new file `app/dependencies.py`:
|
|
|
|
```python
|
|
from fastapi import Security, HTTPException
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from app.config import settings
|
|
|
|
bearer = HTTPBearer(auto_error=False)
|
|
|
|
def verify_api_key(creds: HTTPAuthorizationCredentials = Security(bearer)):
|
|
if not settings.API_KEY:
|
|
# API_KEY not configured — auth is disabled (development mode)
|
|
return
|
|
if creds is None or creds.credentials != settings.API_KEY:
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
```
|
|
|
|
#### Step 4 — Apply the dependency to all routers
|
|
|
|
In each file under `app/routers/`, add `verify_api_key` as a dependency on the router so every endpoint in that file is protected:
|
|
|
|
```python
|
|
from fastapi import APIRouter, Depends
|
|
from app.dependencies import verify_api_key
|
|
|
|
router = APIRouter(dependencies=[Depends(verify_api_key)])
|
|
```
|
|
|
|
Apply this to every router file. Do not add it per-endpoint — the router-level dependency is cleaner and ensures nothing is accidentally missed.
|
|
|
|
#### Step 5 — Update the tests
|
|
|
|
Any test that calls a protected endpoint now needs an `Authorization: Bearer <key>` header — OR the `API_KEY` setting can be left empty (the default), which disables the check. Verify which approach your test setup uses. If tests use a test client with a settings override, ensure `API_KEY` is either empty (disables check) or consistently set to a test value and the header is passed.
|
|
|
|
Run the tests:
|
|
|
|
```bash
|
|
pytest
|
|
```
|
|
|
|
Fix any 401 failures before proceeding.
|
|
|
|
---
|
|
|
|
### 2. Add rate limiting (`app/main.py` and `app/routers/`)
|
|
|
|
#### Step 1 — Install slowapi
|
|
|
|
```bash
|
|
uv add slowapi
|
|
```
|
|
|
|
Or add `slowapi` to `pyproject.toml` dependencies and run `uv sync`.
|
|
|
|
#### Step 2 — Configure the limiter in `main.py`
|
|
|
|
```python
|
|
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
from slowapi.util import get_remote_address
|
|
from slowapi.errors import RateLimitExceeded
|
|
|
|
limiter = Limiter(key_func=get_remote_address)
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
```
|
|
|
|
#### Step 3 — Apply limits to endpoints
|
|
|
|
Add rate limit decorators to the endpoints most at risk:
|
|
|
|
- All write endpoints (`POST`, `PUT`, `PATCH`, `DELETE`): `@limiter.limit("10/minute")`
|
|
- The timetable endpoint (expensive): `@limiter.limit("30/minute")`
|
|
- Other read endpoints: `@limiter.limit("60/minute")`
|
|
|
|
Example:
|
|
|
|
```python
|
|
from app.main import limiter
|
|
from fastapi import Request
|
|
|
|
@router.post("/sessions")
|
|
@limiter.limit("10/minute")
|
|
async def create_session(request: Request, ...):
|
|
...
|
|
```
|
|
|
|
Note: `slowapi` requires the `request: Request` parameter to be present in the function signature for the decorator to work. Add it to any endpoint that doesn't already have it. It does not need to be used inside the function body.
|
|
|
|
#### Step 4 — Verify
|
|
|
|
Run the tests:
|
|
|
|
```bash
|
|
pytest
|
|
```
|
|
|
|
Rate limiting may interfere with tests that call endpoints in a tight loop. If so, configure the limiter to be disabled in the test environment (e.g. check for a `TESTING=true` env var and set `enabled=False` on the limiter).
|
|
|
|
---
|
|
|
|
### 3. Bound the `limit` query parameter (`app/routers/sessions.py`)
|
|
|
|
#### Step 1 — Read the file first
|
|
|
|
Find the `get_sessions` endpoint (or equivalent) and locate the `limit` parameter.
|
|
|
|
#### Step 2 — Add the bound
|
|
|
|
Replace the bare `limit: int = 200` with a constrained `Query`:
|
|
|
|
```python
|
|
from fastapi import Query
|
|
|
|
async def get_sessions(
|
|
limit: int = Query(default=200, ge=1, le=1000),
|
|
...
|
|
):
|
|
```
|
|
|
|
This rejects any `limit` value below 1 or above 1000 with a FastAPI 422 validation error automatically — no extra code needed.
|
|
|
|
#### Step 3 — Verify
|
|
|
|
Run the full test suite one final time:
|
|
|
|
```bash
|
|
pytest
|
|
```
|
|
|
|
All tests must pass before declaring this plan complete. Do not move on to Plan 5 until the suite is green.
|