gmao/app_new/core/authorization.py
root d3ddfdb974
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Centralise les rôles et les permissions
2026-08-14 18:53:03 +00:00

150 lines
5.7 KiB
Python

"""Contrôle d'accès centralisé de l'application.
La politique s'applique à toutes les routes enregistrées. Les décorateurs locaux
restent utiles pour documenter les cas particuliers, mais une route oubliée ne
retombe plus sur le simple fait d'être connecté.
"""
from functools import wraps
from flask import abort, request
from flask_login import current_user
ROLE_ALIASES = {
"super_admin": "admin",
"admin": "admin",
"chef": "responsable_gmao",
"responsable": "responsable_gmao",
"responsable_gmao": "responsable_gmao",
"tech": "technicien",
"technician": "technicien",
"technicien": "technicien",
"assistant_prevention": "assistant_prevention",
"requester": "demandeur",
"user": "demandeur",
"demandeur": "demandeur",
"viewer": "lecture",
"lecture": "lecture",
}
ROLE_LABELS = {
"admin": "Administrateur système",
"responsable_gmao": "Responsable GMAO",
"technicien": "Technicien",
"assistant_prevention": "Assistant de prévention",
"demandeur": "Demandeur",
"lecture": "Lecture seule",
}
PERMISSIONS_BY_ROLE = {
"admin": {"*"},
"responsable_gmao": {
"dashboard.view", "intervention.view", "intervention.create", "intervention.manage",
"patrimoine.view", "patrimoine.manage", "planning.view", "planning.manage",
"stock.view", "stock.manage", "contract.view", "contract.manage",
"prevention.view", "prevention.manage", "export.use",
},
"technicien": {
"dashboard.view", "intervention.view", "intervention.create", "intervention.manage",
"patrimoine.view", "patrimoine.manage", "planning.view", "planning.manage",
"stock.view", "stock.manage", "contract.view", "prevention.view", "export.use",
},
"assistant_prevention": {
"dashboard.view", "intervention.view", "intervention.create", "patrimoine.view",
"planning.view", "prevention.view", "prevention.manage", "export.use",
},
"demandeur": {"dashboard.view", "intervention.view", "intervention.create"},
"lecture": {"dashboard.view", "intervention.view", "patrimoine.view", "planning.view", "contract.view"},
}
PUBLIC_ENDPOINTS = {
"auth.login", "health.health", "static",
"setup_wizard.index", "setup_wizard.api_progress", "setup_wizard.api_save_step",
"setup_wizard.api_complete", "setup_wizard.api_test_ent", "setup_wizard.api_test_pronote",
}
ADMIN_BLUEPRINTS = {
"admin", "ai_config", "gmao_config", "logs", "status", "yeastar",
"outlook_auth", "outlook_dashboard", "outlook_pages", "outlook_sync", "ent", "pronote",
}
PATRIMOINE_BLUEPRINTS = {
"equipments", "equipments_meters", "equipments_documents", "equipments_restrictions",
"equipments_scheduled", "buildings", "zones", "rooms", "room_types", "wizard", "lots",
}
PLANNING_BLUEPRINTS = {"planning", "scheduler", "interventions_planning"}
STOCK_BLUEPRINTS = {"parts", "meters"}
CONTRACT_BLUEPRINTS = {"companies", "contracts", "services"}
PREVENTION_BLUEPRINTS = {"trainings", "constraints"}
def canonical_role(role):
return ROLE_ALIASES.get((role or "").strip().lower(), "lecture")
def has_permission(permission, user=None):
user = user or current_user
if not getattr(user, "is_authenticated", False):
return False
permissions = PERMISSIONS_BY_ROLE.get(canonical_role(user.role), set())
return "*" in permissions or permission in permissions
def permission_required(permission):
def decorator(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not has_permission(permission):
abort(403)
return view(*args, **kwargs)
return wrapped
return decorator
def required_permission(endpoint, method):
"""Déduit la permission minimale d'une route enregistrée."""
if not endpoint:
return None
blueprint = endpoint.split(".", 1)[0]
mutating = method not in {"GET", "HEAD", "OPTIONS"}
if blueprint in ADMIN_BLUEPRINTS:
return "system.admin"
if blueprint == "auth":
return "system.admin" if endpoint not in {"auth.profile", "auth.change_own_password", "auth.logout"} else None
if blueprint == "setup_wizard":
return "system.admin"
if blueprint in PATRIMOINE_BLUEPRINTS:
return "patrimoine.manage" if mutating else "patrimoine.view"
if blueprint in PLANNING_BLUEPRINTS:
return "planning.manage" if mutating else "planning.view"
if blueprint == "interventions":
if endpoint in {"interventions.create", "interventions.create_for_group"}:
return "intervention.create"
return "intervention.manage" if mutating else "intervention.view"
if blueprint in STOCK_BLUEPRINTS:
return "stock.manage" if mutating else "stock.view"
if blueprint in CONTRACT_BLUEPRINTS:
return "contract.manage" if mutating else "contract.view"
if blueprint in PREVENTION_BLUEPRINTS:
return "prevention.manage" if mutating else "prevention.view"
if blueprint == "exports":
return "export.use"
if blueprint in {"dashboard", "notifications", "messagerie", "chatbot"}:
return "dashboard.view"
return "system.admin" if mutating else "dashboard.view"
def enforce_route_permission():
"""Garde global appelé avant chaque requête."""
endpoint = request.endpoint
if endpoint in PUBLIC_ENDPOINTS or endpoint == "static":
return None
# L'API REST applique sa clé ou sa session dans son propre décorateur.
if endpoint and endpoint.startswith("api_v1."):
return None
if not current_user.is_authenticated:
return None # Flask-Login conserve la redirection habituelle des vues protégées.
permission = required_permission(endpoint, request.method)
if permission and not has_permission(permission):
abort(403)
return None