refactor(backend): extract shared helper modules per architecture rules

- Add backend/ai/utils.py — parse_classification, parse_suggestions, strip_code_fences
  shared by all AI providers; removes duplicated private functions from
  anthropic_provider.py and openai_provider.py
- Add backend/deps/utils.py — get_client_ip, parse_uuid request-parsing helpers;
  removes local _ip() variants from admin.py, auth.py, shares.py, folders.py
- Add backend/storage/exceptions.py — canonical CloudConnectionError definition;
  all routers and backends import from here instead of redefining
- Move validate_password_strength to backend/services/auth.py; removes duplicated
  _validate_password_strength from admin.py and auth.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-02 16:10:35 +02:00
parent 89f8d5a654
commit a548266461
14 changed files with 171 additions and 232 deletions
+8 -52
View File
@@ -23,7 +23,6 @@ Security invariants:
"""
from __future__ import annotations
import re
import uuid
from datetime import datetime
from typing import Optional
@@ -36,8 +35,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from db.models import CloudConnection, Document, Quota, RefreshToken, Topic, User
from deps.auth import get_current_admin
from deps.db import get_db
from deps.utils import get_client_ip
from services.audit import write_audit_log
from services.auth import hash_password, revoke_all_refresh_tokens, verify_password
from services.auth import hash_password, revoke_all_refresh_tokens, validate_password_strength, verify_password
from storage import get_storage_backend, get_storage_backend_for_document
router = APIRouter(prefix="/api/admin", tags=["admin"])
@@ -46,28 +46,6 @@ router = APIRouter(prefix="/api/admin", tags=["admin"])
_DEFAULT_QUOTA_BYTES = 104857600 # 100 MB free-tier default (D-06)
_PASSWORD_DETAIL = (
"Password must be at least 12 characters and include uppercase, "
"lowercase, a number, and a special character."
)
# ── IP extraction helper ──────────────────────────────────────────────────────
def _ip(request: Request) -> Optional[str]:
"""Extract best-effort client IP from request for audit logging.
TRUST BOUNDARY: X-Forwarded-For is a client-controlled header and can be
forged by any caller. This value is used for forensic audit logging only —
not for authentication or access control decisions. In production, deploy
behind a trusted reverse proxy (e.g. nginx with
`proxy_set_header X-Forwarded-For $remote_addr;`) which overwrites this
header with the real remote IP before it reaches FastAPI, or use a
trusted-proxy middleware that validates the source CIDR.
"""
return request.headers.get("X-Forwarded-For") or (
request.client.host if request.client else None
)
# ── Safe response helper ──────────────────────────────────────────────────────
@@ -90,25 +68,6 @@ def _user_to_dict(user: User) -> dict:
}
# ── Password strength helper ──────────────────────────────────────────────────
def _validate_password_strength(password: str) -> None:
"""Raise ValueError with the spec message if password fails any strength rule.
Rules (AUTH-01): min 12 chars, has uppercase, has lowercase, has digit,
has special char (non-alphanumeric).
"""
if len(password) < 12:
raise ValueError(_PASSWORD_DETAIL)
if not re.search(r"[A-Z]", password):
raise ValueError(_PASSWORD_DETAIL)
if not re.search(r"[a-z]", password):
raise ValueError(_PASSWORD_DETAIL)
if not re.search(r"[0-9]", password):
raise ValueError(_PASSWORD_DETAIL)
if not re.search(r"[^A-Za-z0-9]", password):
raise ValueError(_PASSWORD_DETAIL)
# ── Request models ────────────────────────────────────────────────────────────
@@ -121,10 +80,7 @@ class UserCreate(BaseModel):
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
try:
_validate_password_strength(v)
except ValueError as exc:
raise ValueError(str(exc)) from exc
validate_password_strength(v)
return v
@@ -264,7 +220,7 @@ async def create_user(
session.add(quota)
await session.flush() # persist User + Quota before audit_log FK references them
# D-13: admin user created event
_ip_addr = _ip(request)
_ip_addr = get_client_ip(request)
await write_audit_log(
session,
event_type="admin.user_created",
@@ -316,7 +272,7 @@ async def update_user_status(
detail="Cannot deactivate the only admin",
)
_ip_addr = _ip(request)
_ip_addr = get_client_ip(request)
user.is_active = body.is_active
if not body.is_active:
@@ -426,7 +382,7 @@ async def update_user_quota(
else None
)
_ip_addr = _ip(request)
_ip_addr = get_client_ip(request)
old_limit = quota.limit_bytes
quota.limit_bytes = body.limit_bytes
session.add(quota)
@@ -471,7 +427,7 @@ async def update_ai_config(
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
_ip_addr = _ip(request)
_ip_addr = get_client_ip(request)
user.ai_provider = body.ai_provider
user.ai_model = body.ai_model
session.add(user)
@@ -532,7 +488,7 @@ async def delete_user(
detail="Cannot delete admin accounts",
)
_ip_addr = _ip(request)
_ip_addr = get_client_ip(request)
# SEC-09 (cloud): purge cloud-stored documents and credentials BEFORE DB delete.
# Must run before MinIO cleanup so that credentials are still available to build