fix: A3–A8 — bloc « fallback admin » de l'audit sécurité (401 sans session, ACL, CSRF)
- A3 : helper `_require_user_id()` (401 sans session) sur profile/password/token/forge ; `PUT /api/user/password` exige `current_password` vérifié ; `/api/user` sorti de la liste CSRF exemptée - A4 : `POST /api/v1/token` → 401 sans session, chemin legacy `user_id=0` supprimé - A5 : CRUD membres d'espace (POST/PUT/DELETE) : session + rôle admin de l'espace (ou admin global), placeholder user créé en `is_admin=0`, GET membres sans session → 401 - A6 : `_require_view` → 404 et `_require_edit` → 401 quand il n'y a pas de session (fin du legacy single-user sur les collections) - A7 : création ET lecture de page → 401 sans session (PermissionManager conservé) ; `/board/api/pages` sorti de `EXCLUDED_PATHS` ; header CSRF manquant ajouté sur setItemIcon (local_workspace) - A8 : seed admin sans mot de passe codé en dur — aléatoire au premier boot loggé une fois, ou `FLOWDECK_ADMIN_PASSWORD` ; re-seed seulement si absent - tests : client connecté par défaut via `_TestSessionAuth` (session + CSRF injectés à la volée, jamais dans le cookie jar → plus de CookieConflict), helper `anon()` sur les 40 tests d'anonymat ; 1016/1016 verts, `ruff check app tests` OK
This commit is contained in:
+61
-1
@@ -5,13 +5,73 @@ suite is safe to run in parallel (``pytest -n auto``): workers never share a
|
||||
database file, and no state leaks between tests.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class _TestSessionAuth(httpx.Auth):
|
||||
"""Session + CSRF injectés à la volée (jamais dans le cookie jar du client).
|
||||
|
||||
- `flowdeck_session` ajouté seulement s'il est absent de la requête (un test
|
||||
peut fournir la sienne via `cookies=`) ;
|
||||
- `csrf_token` idem, et l'en-tête `X-CSRF-Token` suit TOUJOURS le cookie
|
||||
courant (le token tourne quand `/api/csrf-token` est appelé) ;
|
||||
- un test qui veut l'anonymat fait `anon(client)` → `client.auth = None`.
|
||||
"""
|
||||
|
||||
CSRF_FALLBACK = "csrf-test-token"
|
||||
|
||||
def __init__(self, session_token: str):
|
||||
self.session_token = session_token
|
||||
|
||||
def auth_flow(self, request):
|
||||
ch = request.headers.get("cookie", "")
|
||||
add = []
|
||||
if "flowdeck_session=" not in ch:
|
||||
add.append(f"flowdeck_session={self.session_token}")
|
||||
if "csrf_token=" not in ch:
|
||||
add.append(f"csrf_token={self.CSRF_FALLBACK}")
|
||||
if add:
|
||||
request.headers["cookie"] = "; ".join(([ch] if ch else []) + add)
|
||||
if "X-CSRF-Token" not in request.headers:
|
||||
m = re.search(r"csrf_token=([^;]+)", request.headers.get("cookie", ""))
|
||||
if m:
|
||||
request.headers["X-CSRF-Token"] = m.group(1)
|
||||
yield request
|
||||
|
||||
|
||||
def login_test_client(tc, user_id: int = 1, login: str = "tester", is_admin: int = 1):
|
||||
"""Connecte un TestClient (A3–A7 : les routes testées exigent une session)."""
|
||||
from app.auth.session import SessionManager
|
||||
from app.db import get_conn
|
||||
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO users (id, login, full_name, is_admin) VALUES (?,?,?,?)",
|
||||
(user_id, login, login.title(), is_admin),
|
||||
)
|
||||
conn.commit()
|
||||
tc.auth = _TestSessionAuth(
|
||||
SessionManager.create_session(
|
||||
{"id": user_id, "login": login, "full_name": login.title(), "is_admin": is_admin}
|
||||
)
|
||||
)
|
||||
return tc
|
||||
|
||||
|
||||
def anon(client):
|
||||
"""Test d'anonymat : plus de session, plus de CSRF par défaut."""
|
||||
client.cookies.clear()
|
||||
client.headers.pop("X-CSRF-Token", None)
|
||||
client.auth = None
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""FastAPI TestClient with a fresh temporary SQLite database."""
|
||||
@@ -55,7 +115,7 @@ def client():
|
||||
from app.main import app
|
||||
init_db()
|
||||
|
||||
yield TestClient(app)
|
||||
yield login_test_client(TestClient(app))
|
||||
|
||||
# Cleanup
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user