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").
117 lines
6.0 KiB
Python
117 lines
6.0 KiB
Python
"""Garde d'accès globale — connexion obligatoire pour tout le moteur, et
|
|
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
|
|
module tente de résoudre request.endpoint — en pratique sans importance
|
|
ici (la résolution se fait à la requête, pas à l'import), mais gardé pour
|
|
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
|
|
|
|
# Endpoints accessibles SANS être connecté — tout le reste exige une
|
|
# session valide. "static" (CSS/JS/images) doit rester public : la page
|
|
# de connexion elle-même en a besoin pour s'afficher.
|
|
_PUBLIC_ENDPOINTS = {
|
|
"static", "login", "login_2fa", "register", "register_2fa", "logout",
|
|
"forgot_password", "reset_password",
|
|
}
|
|
|
|
# Endpoints de gestion de COMPTE (routes/auth/profile.py) — connexion
|
|
# requise, mais indépendants d'avoir un projet ou non (voir onboarding
|
|
# guidé ci-dessous) : un compte pas encore passé par /onboarding doit
|
|
# quand même pouvoir changer son mot de passe, son email, ou supprimer
|
|
# son compte.
|
|
_REACHABLE_WITHOUT_PROJECT = {
|
|
"onboarding_new", "profile", "profile_update_name", "profile_update_email",
|
|
"profile_regenerate_recovery_codes", "profile_change_password", "profile_delete",
|
|
}
|
|
|
|
|
|
@app.before_request
|
|
def _require_login_and_enforce_project_isolation():
|
|
endpoint = request.endpoint
|
|
if endpoint is None or endpoint in _PUBLIC_ENDPOINTS:
|
|
return None
|
|
|
|
user_id = session.get("user_id")
|
|
if not user_id:
|
|
return redirect(url_for("login", next=request.path))
|
|
|
|
user = auth.get_user_by_id(user_id)
|
|
if not user:
|
|
# Compte supprimé/introuvable depuis la dernière requête : la
|
|
# session ne doit jamais rester "connectée" dans le vide.
|
|
session.clear()
|
|
return redirect(url_for("login"))
|
|
g.current_user = user
|
|
|
|
# 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"]))
|
|
# Onboarding guidé (voir routes/onboarding/onboarding_new.py) : un
|
|
# compte "user" fraîchement inscrit n'a plus de projet créé
|
|
# d'office (voir routes/auth/register_2fa.py) — tant qu'il n'en a
|
|
# pas, seules ces routes restent atteignables (toutes exigent déjà
|
|
# 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"))
|
|
# 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
|
|
# une version SIMPLIFIÉE (juste les écrans, pas d'onglets Objets/
|
|
# Éléments de jeu/Variables) — rien à bloquer ici, la restriction
|
|
# se fait au niveau du rendu, pas de l'accès à la route.
|
|
if endpoint == "game_delete":
|
|
# Un compte "user" n'a qu'UN SEUL projet, et aucun moyen d'en
|
|
# recréer un une fois supprimé (games_new le renvoie toujours
|
|
# vers project_slug, même si le dossier n'existe plus) —
|
|
# bloqué plutôt que de risquer de le laisser sans projet du
|
|
# tout. Un admin, lui, peut toujours supprimer ses jeux comme
|
|
# avant (aucune restriction ci-dessus pour ce rôle).
|
|
abort(403)
|
|
return None
|