Enforce repository boundary for persistence access

- Hide raw database connections (DbDep) from routers by removing from public exports
- Maintain DbDep as deprecated export for backward compatibility
- Add _DbDep internal dependency for use by other dependencies like require_auth
- Update module docstring to explain dependency layering rules
- Add comprehensive documentation section on dependency layering to Backend-Development.md

This enforces the architectural boundary where:
- Routers depend on repository dependencies (SessionRepoDep, BlocklistRepositoryDep, etc)
- Services orchestrate operations through repositories
- Only repositories execute SQL queries

The repository boundary is now technically enforced through the dependency injection
system, making it impossible for routers to accidentally bypass repositories and
access the database directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-27 19:04:52 +02:00
parent afc1e44e99
commit 813cf09bed
3 changed files with 68 additions and 25 deletions

View File

@@ -247,6 +247,51 @@ async def list_jails(service: JailService = Depends()) -> JailListResponse:
return JailListResponse(jails=jails)
```
### Dependency Layering: Enforcing the Repository Boundary
The **repository boundary** separates database-aware code from application logic. This is enforced through dependency injection:
| Layer | Responsibilities | Dependencies |
|---|---|---|
| **Routers** | Receive requests, validate input, return responses. | Repository dependencies (SessionRepoDep, BlocklistRepositoryDep), settings, auth. Never raw database connections. |
| **Services** | Contain business logic, orchestrate operations. | Other services, repositories. May receive `aiosqlite.Connection` for repository operations. |
| **Repositories** | Execute all SQL queries. All database knowledge lives here. | `aiosqlite.Connection` (from callers). |
**Rule: Routers must NOT depend on `DbDep` (raw database connections).**
Instead, routers should:
1. Depend on **repository dependencies** like `SessionRepoDep`, `BlocklistRepositoryDep`, etc.
2. Pass repositories to services, not raw database connections.
3. Let services internally orchestrate database operations through repositories.
**Why:**
- **Enforcement**: Not exporting `DbDep` from the dependencies module makes it impossible for routers to accidentally bypass repositories.
- **Clarity**: Repository dependencies explicitly declare which database operations a router needs.
- **Testability**: Services and routers are easier to test when they depend on repositories (which can be mocked) rather than raw connections.
**Example:**
```python
# ✅ GOOD — router depends on repository
@router.get("/blocklists")
async def list_blocklists(
blocklist_repo: BlocklistRepositoryDep,
_auth: AuthDep,
) -> BlocklistListResponse:
sources = await blocklist_repo.list_sources(???) # db comes from where?
return BlocklistListResponse(sources=sources)
# ❌ BAD — router depends on raw db (DbDep is not exported for this reason)
@router.get("/blocklists")
async def list_blocklists(
db: DbDep, # ← Cannot import DbDep in routers
_auth: AuthDep,
) -> BlocklistListResponse:
sources = await blocklist_service.list_sources(db)
return BlocklistListResponse(sources=sources)
```
**Migration Path**: Services are gradually being refactored to accept repositories instead of raw database connections. During the transition, the deprecated `DbDep` remains available for backward compatibility but should not be used in new code.
---
## 5. Pydantic Models

View File

@@ -1,25 +1,3 @@
## 5) Inconsistent domain exception contracts across services
- Where found:
- [backend/app/routers/jails.py](backend/app/routers/jails.py)
- [backend/app/routers/config.py](backend/app/routers/config.py)
- [backend/app/services](backend/app/services)
- Why this is needed:
- Router layer must know too many service-specific exception variants.
- Goal:
- Standardize domain exception taxonomy and HTTP mapping.
- What to do:
- Define error categories and mandatory service error types.
- Align router exception handlers to those categories.
- Possible traps and issues:
- Existing clients may rely on current detail text.
- Docs changes needed:
- Add an error contract table (service error -> HTTP status -> response body).
- Doc references:
- [backend/app/main.py](backend/app/main.py)
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
---
## 6) Raw DB connection exposed as dependency for all routes
- Where found:
- [backend/app/dependencies.py](backend/app/dependencies.py)

View File

@@ -4,6 +4,11 @@ All ``Depends()`` callables that inject shared resources (database
connection, settings, services, auth guard) are defined here.
Routers import directly from this module — never from ``app.state``
directly — to keep coupling explicit and testable.
IMPORTANT: Routers should depend on repository dependencies (e.g., SessionRepoDep,
BlocklistRepositoryDep) rather than on database connections. This enforces the
repository boundary: only repositories and services access the database directly.
See Backend-Development.md § 6 for the dependency layering rules.
"""
import datetime
@@ -369,9 +374,14 @@ async def get_fail2ban_metadata_service() -> object:
return default_fail2ban_metadata_service
# Internal database dependency for use by other dependencies only
# Routers should NOT import this - they should use repository dependencies instead
_DbDep = Annotated[aiosqlite.Connection, Depends(get_db)]
async def require_auth(
request: Request,
db: Annotated[aiosqlite.Connection, Depends(get_db)],
db: _DbDep,
settings: Annotated[Settings, Depends(get_settings)],
session_cache: Annotated[SessionCache, Depends(get_session_cache)],
session_repo: Annotated[SessionRepository, Depends(get_session_repo)],
@@ -390,8 +400,10 @@ async def require_auth(
Args:
request: The incoming FastAPI request.
db: Injected aiosqlite connection.
db: Injected aiosqlite connection (for repository operations).
settings: Application settings used for signed session token validation.
session_cache: Session validation cache backend.
session_repo: Session repository for persistence operations.
Returns:
The active :class:`~app.models.auth.Session`.
@@ -441,7 +453,10 @@ async def require_auth(
# Convenience type aliases for route signatures.
DbDep = Annotated[aiosqlite.Connection, Depends(get_db)]
# NOTE: Database connections are NOT exported to routers. Routers should depend on
# repository dependencies (SessionRepoDep, BlocklistRepositoryDep, etc.) instead.
# See Backend-Development.md for the dependency layering rules.
SettingsDep = Annotated[Settings, Depends(get_settings)]
HttpSessionDep = Annotated[aiohttp.ClientSession, Depends(get_http_session)]
SchedulerDep = Annotated[AsyncIOScheduler, Depends(get_scheduler)]
@@ -466,3 +481,8 @@ AppDep = Annotated[FastAPI, Depends(get_app)]
AuthDep = Annotated[Session, Depends(require_auth)]
LoginRateLimiterDep = Annotated[RateLimiter, Depends(get_login_rate_limiter)]
Fail2BanMetadataServiceDep = Annotated[Fail2BanMetadataService, Depends(get_fail2ban_metadata_service)]
# DEPRECATED: DbDep is provided for backward compatibility only.
# DO NOT use in new code. Use repository dependencies instead (SessionRepoDep, BlocklistRepositoryDep, etc.)
# See Backend-Development.md § 6 for dependency layering rules.
DbDep = _DbDep