fix: A3–A8 — bloc « fallback admin » de l'audit sécurité (401 sans session, ACL, CSRF)
FlowDeck CI / lint (push) Successful in 1m49s
FlowDeck CI / test (push) Successful in 21m47s
FlowDeck CI / docker (push) Successful in 1m45s

- 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:
2026-09-30 22:04:13 -04:00
parent e6c1f7dbb3
commit d125eb399e
36 changed files with 260 additions and 67 deletions
+61 -1
View File
@@ -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: