""" auth_service.py — Fonctions de validation des droits d'accès, centralisées et réutilisées par toutes les routes (Bloc 1 - C9 : taux de réutilisation > 80 % via services/ ; Bloc 1 - C8 : "vérification JWT à chaque requête"). Toute route sensible doit passer par current_user() / require_login() / require_admin() plutôt que de relire directement flask.session, afin que la règle d'autorisation soit définie à un seul endroit. """ from functools import wraps from flask import session, jsonify, request from config import Config def current_user(): """Retourne l'utilisateur en session (dict issu du token OIDC) ou None.""" return session.get("user") def is_authenticated(): return current_user() is not None _UNSET = object() def is_admin(user=_UNSET): """True si l'utilisateur appartient au groupe Keycloak /admin. Appelé sans argument, lit l'utilisateur courant dans la session Flask (nécessite un contexte de requête actif). Appelé avec un argument explicite (y compris None), ce dict/None est utilisé tel quel — utile pour tester la fonction hors contexte de requête (cf. UT-11). """ if user is _UNSET: user = current_user() if not user: return False return Config.ADMIN_GROUP in user.get("groups", []) def require_login(view_func): """Décorateur : bloque l'accès si aucune session utilisateur valide.""" @wraps(view_func) def wrapper(*args, **kwargs): if not is_authenticated(): return jsonify({"error": "unauthorized"}), 401 return view_func(*args, **kwargs) return wrapper def require_admin(view_func): """Décorateur : bloque l'accès si l'utilisateur n'a pas le groupe /admin. Utilisé par toutes les routes de modification (annonces, fichiers, blocks). Aucune route admin n'est accessible sans token valide et sans appartenance au groupe Keycloak /admin (mitigation du risque "accès non autorisé à /admin", cf. tableau OWASP du dossier technique). """ @wraps(view_func) def wrapper(*args, **kwargs): user = current_user() if not user: return jsonify({"error": "unauthorized"}), 401 if not is_admin(user): return jsonify({"error": "forbidden"}), 403 return view_func(*args, **kwargs) return wrapper def client_ip(): """IP cliente, en tenant compte du proxy Caddy (X-Forwarded-For).""" forwarded = request.headers.get("X-Forwarded-For", "") if forwarded: return forwarded.split(",")[0].strip() return request.remote_addr