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
+41 -13
View File
@@ -1,9 +1,10 @@
"""Garde d'accès globale — connexion obligatoire pour tout le moteur, et
isolation par utilisateur d'un SEUL projet (sauf le rôle "admin",
illimité comme avant l'authentification). Un seul before_request plutôt
qu'un décorateur à poser sur chacune des ~80 routes existantes : moins de
risque d'en oublier une, et aucun fichier de routes existant n'a besoin
d'être modifié.
isolation par PROPRIÉTAIRE de projet, pour TOUS les rôles (y compris
"admin" — voir plus bas, un admin garde d'autres privilèges mais plus
aucun accès aux projets des autres comptes, faille corrigée). Un seul
before_request plutôt qu'un décorateur à poser sur chacune des ~80 routes
existantes : moins de risque d'en oublier une, et aucun fichier de routes
existant n'a besoin d'être modifié.
Cet import doit avoir lieu APRÈS `import routes` (voir app.py/conftest.py)
pour que `app.url_map` connaisse déjà toutes les routes au moment où ce
@@ -13,6 +14,7 @@ rester cohérent avec l'ordre d'import du reste du moteur."""
from flask import g, redirect, request, session, url_for, abort
import auth
from db.games.project_slug import split_slug
from .flask_app import app
@@ -53,11 +55,40 @@ def _require_login_and_enforce_project_isolation():
return redirect(url_for("login"))
g.current_user = user
# Un compte "user" n'a accès qu'à SON SEUL projet (project_slug) — un
# "admin" reste illimité, exactement comme avant l'authentification.
# Les routes de gestion multi-jeux (page d'accueil, "+ Nouveau jeu")
# n'ont pas leur place pour un compte à projet unique : redirigées
# directement vers son propre tableau de bord plutôt qu'un 403 sec.
# Propriété d'un projet = le segment "propriétaire" de son slug (voir
# db/games/project_slug.py — projects/<owner_folder>/<projet>/,
# owner_folder est l'id du compte qui l'a créé, voir
# db/games/create_game.py) comparé à l'id du compte CONNECTÉ — pour
# TOUS les rôles, admin compris. Corrige une faille : auparavant ce
# contrôle (comme tout le reste de cette fonction) était sauté pour
# "admin", qui pouvait donc ouvrir/modifier/supprimer le projet de
# N'IMPORTE QUEL autre compte en connaissant son slug.
#
# Un slug "à plat" (un seul segment, `split_slug` renvoie alors
# `project_part=None`) n'a jamais été rattaché à un compte précis —
# avant l'introduction de project_slug, c'était déjà la norme
# (voir scripts/migrate_flat_project_slugs.py), et `db.create_game()`
# sans `owner_folder` produit encore ce format aujourd'hui (utilisé
# par une bonne partie de la suite de tests comme jeu jetable, sans
# compte associé). Réservé au rôle "admin" (repli sur le comportement
# "illimité" historique pour ce cas précis) — jamais un "user", qui
# n'a par construction aucun projet à plat légitime.
slug = request.view_args.get("slug") if request.view_args else None
if slug is not None:
owner_folder, project_part = split_slug(slug)
if project_part is None:
if user["role"] != "admin":
abort(403)
elif owner_folder != str(user["id"]):
abort(403)
# Un compte "user" n'a accès qu'à SON SEUL projet (project_slug,
# simple raccourci de confort ici — la sécurité elle-même est déjà
# assurée ci-dessus) — un "admin" peut en avoir plusieurs, exactement
# comme avant l'authentification. Les routes de gestion multi-jeux
# (page d'accueil, "+ Nouveau jeu") n'ont pas leur place pour un
# compte à projet unique : redirigées directement vers son propre
# tableau de bord plutôt qu'un 403 sec.
if user["role"] != "admin":
if endpoint in ("index", "games_new") and user.get("project_slug"):
return redirect(url_for("game_dashboard", slug=user["project_slug"]))
@@ -68,9 +99,6 @@ def _require_login_and_enforce_project_isolation():
# une connexion, absentes de _PUBLIC_ENDPOINTS).
if not user.get("project_slug") and endpoint not in _REACHABLE_WITHOUT_PROJECT:
return redirect(url_for("onboarding_new"))
slug = request.view_args.get("slug") if request.view_args else None
if slug is not None and slug != user.get("project_slug"):
abort(403)
# Type d'onboarding "restreint" (quiz/embranchement/rpg, voir
# db/games/game_type_catalog.py) : game_dashboard reste atteignable
# comme pour "custom", mais routes/games/game_dashboard.py y rend