gmao/app_new/core/services/rbac.py
root 20362fb31e
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Renforcer le RBAC et séparer les administrateurs
2026-08-21 18:10:29 +00:00

87 lines
3.6 KiB
Python

"""Services transactionnels pour la gestion des rôles RBAC.
Les fonctions de ce module ne valident ni ne commitent la transaction appelante.
Cela permet aux routes d'enregistrer simultanément l'utilisateur et ses accès.
"""
from ...extensions import db
from ..models.rbac import Role, UserRole
from ..models.user import User
class LastSuperAdminError(ValueError):
"""Opération qui supprimerait le dernier super administrateur actif."""
def ensure_super_admin_survives(user, *, will_be_active=None, deleting=False):
"""Bloque la désactivation/suppression du dernier super-admin actif."""
if not any(link.role and link.role.slug == "super_admin" and link.role.is_active for link in user.role_links):
return
if not deleting and (will_be_active is None or will_be_active):
return
count = UserRole.query.join(Role).join(User, User.id == UserRole.user_id).filter(
Role.slug == "super_admin", Role.is_active.is_(True),
db.or_(User.is_active.is_(True), User.is_active.is_(None))
).count()
if count <= 1:
raise LastSuperAdminError("Impossible de désactiver ou supprimer le dernier super administrateur actif.")
def _is_last_super_admin(user, selected_ids):
super_role = Role.query.filter_by(slug="super_admin").first()
if not super_role or super_role.id not in {int(value) for value in selected_ids}:
if super_role and any(link.role_id == super_role.id for link in user.role_links):
count = UserRole.query.join(Role).join(User, User.id == UserRole.user_id).filter(
Role.slug == "super_admin", Role.is_active.is_(True),
db.or_(User.is_active.is_(True), User.is_active.is_(None))
).count()
return count <= 1
return False
def replace_user_roles(user, role_ids, assigned_by=None):
"""Remplace l'ensemble des rôles actifs d'un utilisateur.
Les liens déjà sélectionnés sont conservés (et donc leur traçabilité), les
liens absents sont retirés et les nouveaux sont ajoutés sans doublon.
``role_ids`` peut être vide. Les rôles inactifs ou inconnus sont ignorés.
La fonction travaille dans la transaction courante et ne committe jamais.
"""
normalized_ids = set()
for value in role_ids or ():
try:
normalized_ids.add(int(value))
except (TypeError, ValueError):
continue
active_roles = {
role.id: role
for role in Role.query.filter(Role.id.in_(normalized_ids), Role.is_active.is_(True)).all()
} if normalized_ids else {}
selected_ids = set(active_roles)
super_role = Role.query.filter_by(slug="super_admin").first()
if super_role and super_role.id in selected_ids and assigned_by is not None:
actor_is_super = any(
link.role and link.role.slug == "super_admin" and link.role.is_active
for link in getattr(assigned_by, "role_links", [])
)
if not actor_is_super:
raise LastSuperAdminError("Seul un super administrateur peut attribuer le rôle super_admin.")
if _is_last_super_admin(user, selected_ids):
raise LastSuperAdminError("Impossible de retirer le dernier super administrateur actif.")
existing = {link.role_id: link for link in list(user.role_links)}
for role_id, link in existing.items():
if role_id not in selected_ids:
db.session.delete(link)
for role_id in selected_ids - set(existing):
db.session.add(UserRole(
user_id=user.id,
role_id=role_id,
assigned_by_id=getattr(assigned_by, "id", assigned_by),
))
return [active_roles[role_id] for role_id in sorted(selected_ids)]