Files
gesthub/web/services/auth_service.py
2026-08-21 09:12:02 +02:00

84 lines
2.5 KiB
Python

"""
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