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:
+14
-1
@@ -4,6 +4,7 @@ import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from conftest import anon, login_test_client
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@@ -40,7 +41,7 @@ def client():
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
yield TestClient(app)
|
||||
yield login_test_client(TestClient(app))
|
||||
|
||||
try:
|
||||
os.unlink(db_path)
|
||||
@@ -88,6 +89,7 @@ def test_board_404(client):
|
||||
|
||||
|
||||
def test_csrf_rejected(client):
|
||||
anon(client)
|
||||
resp = client.post("/api/move?owner=x&repo=y&issue_id=1&column=Test")
|
||||
assert resp.status_code == 403
|
||||
|
||||
@@ -134,6 +136,7 @@ def test_card_detail_html(client):
|
||||
|
||||
|
||||
def test_create_issue_api(client):
|
||||
anon(client)
|
||||
resp = client.post("/api/issues/test/test?title=Test%20Issue&body=Test%20body")
|
||||
# 403 CSRF or 500 if Gitea down
|
||||
assert resp.status_code in (403, 500)
|
||||
@@ -201,11 +204,13 @@ def test_get_ai_keywords(client):
|
||||
|
||||
|
||||
def test_csrf_protects_properties_post(client):
|
||||
anon(client)
|
||||
resp = client.post("/board/api/properties/test/test?name=Priority&prop_type=select")
|
||||
assert resp.status_code == 403 # CSRF
|
||||
|
||||
|
||||
def test_csrf_protects_sync(client):
|
||||
anon(client)
|
||||
resp = client.post("/board/api/sync/test/test")
|
||||
assert resp.status_code == 403 # CSRF
|
||||
|
||||
@@ -1354,6 +1359,7 @@ def _create_regular_session():
|
||||
|
||||
|
||||
def test_admin_list_users_unauthorized(client):
|
||||
anon(client)
|
||||
"""GET /api/admin/users — 403 without admin session."""
|
||||
resp = client.get("/api/admin/users")
|
||||
assert resp.status_code == 403
|
||||
@@ -1588,6 +1594,7 @@ def test_admin_stats(client):
|
||||
|
||||
|
||||
def test_admin_stats_unauthorized(client):
|
||||
anon(client)
|
||||
"""GET /api/admin/stats — 403 for non-admin."""
|
||||
resp = client.get("/api/admin/stats")
|
||||
assert resp.status_code == 403
|
||||
@@ -1663,6 +1670,7 @@ def test_gitea_status_with_expired_token(client):
|
||||
|
||||
|
||||
def test_gitea_disconnect_no_auth(client):
|
||||
anon(client)
|
||||
"""DELETE /api/gitea/disconnect — 401 without session."""
|
||||
resp = client.delete("/api/gitea/disconnect")
|
||||
assert resp.status_code == 401
|
||||
@@ -1883,6 +1891,7 @@ def test_auth_user_authenticated(client):
|
||||
|
||||
|
||||
def test_auth_user_unauthenticated(client):
|
||||
anon(client)
|
||||
"""GET /auth/user — returns authenticated=false without session."""
|
||||
resp = client.get("/auth/user")
|
||||
assert resp.status_code == 200
|
||||
@@ -1943,6 +1952,7 @@ def test_gitea_private_pages_list_no_auth(client):
|
||||
|
||||
|
||||
def test_gitea_private_pages_create_no_auth(client):
|
||||
anon(client)
|
||||
"""POST private-pages — 401 without session."""
|
||||
resp = client.post("/api/gitea/projects/owner/repo/private-pages", json={"title": "Test"})
|
||||
assert resp.status_code == 401
|
||||
@@ -2019,6 +2029,7 @@ def test_page_renders_breadcrumb_data(client):
|
||||
|
||||
|
||||
def test_landing_page_no_auth(client):
|
||||
anon(client)
|
||||
"""Visiting / without auth shows the landing page."""
|
||||
resp = client.get("/", follow_redirects=False)
|
||||
assert resp.status_code == 200
|
||||
@@ -2095,6 +2106,7 @@ def test_register_duplicate_rejected(client):
|
||||
|
||||
|
||||
def test_session_expired_redirect(client):
|
||||
anon(client)
|
||||
"""Unauthenticated access to protected page redirects with expired param."""
|
||||
resp = client.get("/workspaces", follow_redirects=False)
|
||||
assert resp.status_code == 302
|
||||
@@ -3464,6 +3476,7 @@ def test_v490_page_mentions_endpoint(client):
|
||||
|
||||
|
||||
def test_v490_notifications_require_auth(client):
|
||||
anon(client)
|
||||
r = client.get("/api/notifications")
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
Reference in New Issue
Block a user