Corrige une faille d'isolation entre comptes et retire l'email des chemins de projet
Build and deploy / test-python (push) Successful in 9m39s
Build and deploy / test-js (push) Successful in 52s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Le rôle "admin" contournait entièrement l'isolation par projet
(core/auth_guard.py) : il pouvait ouvrir/modifier/supprimer le jeu de
n'importe quel autre compte en connaissant son slug, et la page d'accueil
listait sans filtrage tous les projets de tous les comptes.

- La propriété d'un projet se vérifie désormais sur le segment
  "propriétaire" du slug (id du compte), pour tous les rôles y compris
  admin — un slug "à plat" (sans compte associé) reste réservé à
  l'admin, comportement historique conservé pour ce cas précis.
- routes/games/index.py ne liste plus que les projets du compte connecté.
- Le dossier propriétaire d'un projet est maintenant l'id numérique du
  compte, plus jamais son email slugifié (visible en clair dans chaque
  URL auparavant) — script de migration fourni et déjà exécuté sur les
  données existantes.
- Changer d'email ne renomme plus aucun dossier (n'en dépend plus).
- Deux nouveaux tests de régression, fixtures corrigées en conséquence.
- README réécrit pour refléter l'état actuel du produit (jeu 2D
  uniquement, plus de traces de l'ancien éditeur "document").
This commit is contained in:
william
2026-09-04 22:52:05 +02:00
parent 50835a18e2
commit 7ebc9b143f
16 changed files with 382 additions and 833 deletions
+12 -10
View File
@@ -97,17 +97,19 @@ def user_client():
@pytest.fixture
def user_game(client, user_client):
"""Un jeu appartenant au compte `user_client` (project_slug assigné,
voir auth.set_project_slug — même mécanisme que l'inscription réelle,
routes/auth/register_2fa.py). Créé via le client ADMIN (`client`,
illimité) puis rattaché, pour ne pas dépendre du parcours
d'inscription complet dans les tests qui n'en ont pas besoin."""
resp = client.post("/games/new", data={"name": "pytest_user_game"}, follow_redirects=False)
assert resp.status_code == 302
slug = resp.headers["Location"].rstrip("/").split("/")[-1]
def user_game(user_client):
"""Un jeu appartenant réellement au compte `user_client` — le dossier
PROPRIÉTAIRE (voir db/games/project_slug.py) doit correspondre à
l'id de CE compte (core/auth_guard.py compare désormais les deux) :
créé directement avec `owner_folder=str(user_id)`, jamais via le
client ADMIN (qui produirait un projet appartenant à l'admin, avec
juste `project_slug` réassigné dessus côté "user" — cassait
l'isolation par propriétaire dès qu'elle a cessé d'être une simple
comparaison de chaîne)."""
with user_client.session_transaction() as sess:
auth.set_project_slug(sess["user_id"], slug)
user_id = sess["user_id"]
slug = db.create_game("pytest_user_game", owner_folder=str(user_id))
auth.set_project_slug(user_id, slug)
yield slug
if os.path.isdir(db.game_dir(slug)):
db.delete_game(slug)
+51 -1
View File
@@ -1,6 +1,7 @@
"""Comptes utilisateurs (auth/) : inscription (nom/prénom/email unique,
mot de passe fort, 2FA TOTP obligatoire), connexion, et isolation par
utilisateur d'un SEUL projet (sauf le rôle "admin", illimité — voir
PROPRIÉTAIRE de projet — pour TOUS les rôles, y compris "admin" (qui
garde seulement le droit d'avoir plusieurs projets À LUI, voir
core/auth_guard.py). Le tout premier compte jamais créé devient
automatiquement admin (auth/create_user.py) : la fixture `client` de
conftest.py en a déjà créé un pour authentifier tous les AUTRES tests du
@@ -200,6 +201,55 @@ def test_non_admin_user_is_isolated_to_their_own_project(anon_client):
_cleanup_project("second")
def _create_isolated_user_with_project(email, name="Jeu de la victime"):
"""Compte + projet créés directement (sans passer par le client HTTP
de test) — combiner ici deux instances de `test_client()` actives en
parallèle (la fixture `client`, admin partagé, ET une seconde pour
créer la victime) fait planter Werkzeug ("Popped wrong app context"),
une limite connue du client de test Flask quand deux contextes de
requête s'entremêlent plutôt que de s'imbriquer proprement."""
user_id = auth.create_user(email, "Sup3r$ecret!", "Test", "Victime")
auth.confirm_totp(user_id)
slug = db.create_game(name, owner_folder=str(user_id))
auth.set_project_slug(user_id, slug)
return auth.get_user_by_id(user_id)
def test_admin_cannot_access_another_users_project(client):
"""Faille corrigée : le rôle "admin" contournait entièrement
l'isolation par projet (voir core/auth_guard.py) — un admin pouvait
ouvrir/modifier/supprimer le jeu de N'IMPORTE QUEL autre compte en
connaissant simplement son slug. `client` (conftest.py) est déjà
connecté en admin ; désormais bloqué exactement comme un "user"."""
victim = _create_isolated_user_with_project("adminvictim@example.com")
try:
assert client.get(f"/game/{victim['project_slug']}").status_code == 403
assert client.get(f"/game/{victim['project_slug']}/screens/1/edit").status_code == 403
assert client.post(f"/game/{victim['project_slug']}/delete").status_code == 403
# Le projet de la victime doit être resté intact (la tentative de
# suppression ci-dessus doit avoir été bloquée AVANT toute écriture).
assert db.game_meta(victim["project_slug"]) is not None
finally:
_cleanup_project("adminvictim@example.com")
def test_admin_home_page_only_lists_their_own_projects(client, game):
"""routes/games/index.py (page "Mes jeux") scannait auparavant TOUT
projects/ sans filtrage (db.list_games()) — n'importe quel admin
voyait donc le slug (et le nom de dossier propriétaire) de chaque
compte. Corrigé : ne renvoie plus que les projets du compte connecté,
admin compris."""
victim = _create_isolated_user_with_project("adminlistvictim@example.com")
try:
resp = client.get("/")
assert resp.status_code == 200
html = resp.get_data(as_text=True)
assert game in html
assert victim["project_slug"] not in html
finally:
_cleanup_project("adminlistvictim@example.com")
def test_non_admin_user_cannot_delete_their_only_project(anon_client):
"""Sans issue de secours (games_new renvoie toujours vers son
project_slug, existant ou non), le supprimer serait un piège sans
+10 -1
View File
@@ -17,7 +17,16 @@ def test_create_game_and_meta(tmp_game_slug_cleanup):
tmp_game_slug_cleanup(slug)
meta = db.game_meta(slug)
assert meta["name"] == "Mon Jeu De Test"
assert slug in [g["slug"] for g in db.list_games()]
def test_list_games_only_returns_the_given_owners_projects(tmp_game_slug_cleanup):
"""db.list_games(owner_folder) ne scanne plus jamais projects/ en
entier (voir routes/games/index.py — faille corrigée : un admin
voyait auparavant les projets de tous les comptes)."""
slug = db.create_game("Jeu propriétaire", owner_folder="999999")
tmp_game_slug_cleanup(slug)
assert slug in [g["slug"] for g in db.list_games("999999")]
assert db.list_games("000000") == []
def test_create_game_deduplicates_slug(tmp_game_slug_cleanup):
+2 -1
View File
@@ -132,7 +132,8 @@ def test_admin_can_create_several_games_via_onboarding(client):
slug2 = resp2.headers["Location"].split("/game/", 1)[1].split("/", 1)[0]
assert slug2 != slug1
slugs = {g["slug"] for g in db.list_games()}
import tests.conftest as conftest_module
slugs = {g["slug"] for g in db.list_games(str(conftest_module._TEST_ADMIN_ID))}
assert slug1 in slugs
assert slug2 in slugs
finally:
+10 -9
View File
@@ -5,7 +5,6 @@ import pyotp
import auth
import db
from db.games.project_slug import build_slug, split_slug
from tests.test_auth import _register, _confirm_2fa, _complete_onboarding, _cleanup_project, anon_client # noqa: F401
@@ -215,7 +214,13 @@ def test_update_email_rejects_an_email_already_used(anon_client):
_cleanup_project("emailtaken2@example.com")
def test_update_email_renames_the_user_project_folder(anon_client):
def test_update_email_never_touches_the_project_folder(anon_client):
"""Le dossier propriétaire d'un projet est nommé d'après l'ID du
compte (voir routes/games/games_new.py, opaque et stable — jamais
l'email, voir scripts/migrate_owner_folders_to_user_id.py pour la
migration qui a retiré l'ancien schéma slugify(email)) : changer
d'adresse email ne doit donc plus jamais renommer quoi que ce soit sur
disque, ni changer project_slug."""
import os
_register(anon_client, "oldmail@example.com")
@@ -231,13 +236,9 @@ def test_update_email_renames_the_user_project_folder(anon_client):
assert auth.get_user_by_email("oldmail@example.com") is None
new_user = auth.get_user_by_email("newmail@example.com")
assert new_user is not None
new_slug = new_user["project_slug"]
# Structure de dossiers par utilisateur (voir db/games/
# project_slug.py) : seul le dossier PROPRIÉTAIRE change de nom,
# le dossier projet (project_part) reste le même.
assert new_slug == build_slug(db.slugify("newmail@example.com"), split_slug(old_slug)[1])
assert not os.path.isdir(db.game_dir(old_slug))
assert os.path.isdir(db.game_dir(new_slug))
# Ni le slug ni le dossier sur disque n'ont bougé.
assert new_user["project_slug"] == old_slug
assert os.path.isdir(db.game_dir(old_slug))
# La connexion se fait désormais avec la nouvelle adresse.
anon_client.post("/logout")