84 lines
2.5 KiB
Python
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
|