diff --git a/app_new/core/authorization.py b/app_new/core/authorization.py index 8db10cc..00a8d49 100644 --- a/app_new/core/authorization.py +++ b/app_new/core/authorization.py @@ -84,6 +84,30 @@ GRANULAR_PERMISSION_CODES = { for module in ("dashboard", "patrimoine", "intervention", "planning", "stock", "contract", "prevention", "user", "system") for action in ("view", "create", "edit", "delete", "validate", "reject", "export", "configure", "manage") } +GRANULAR_PERMISSION_CODES |= { + "gmao_config.view", "gmao_config.configure", + "watchdog_dnd.view", "watchdog_dnd.configure", + "integration.ent.view", "integration.ent.configure", + "integration.outlook.view", "integration.outlook.configure", + "integration.pronote.view", "integration.pronote.configure", + "integration.yeastar.view", "integration.yeastar.configure", + "user.view", "user.create", "user.edit", "user.delete", "user.manage", + "role.view", "role.create", "role.edit", "role.archive", "role.manage", + "audit.view", "audit.export", "system.view", "system.configure", +} + +PERMISSION_LABELS = { + "gmao_config": "Configuration GMAO", + "watchdog_dnd": "Watchdog DND", + "integration.ent": "Intégration ENT", + "integration.outlook": "Intégration Outlook", + "integration.pronote": "Intégration Pronote", + "integration.yeastar": "Intégration Yeastar", + "user": "Utilisateurs", + "role": "Rôles et permissions", + "audit": "Journaux d'audit", + "system": "Système", +} def canonical_role(role): @@ -160,6 +184,26 @@ def required_permission(endpoint, method): action = "validate" return f"stock.{action}" + if blueprint == "gmao_config": + return "gmao_config.configure" if mutating else "gmao_config.view" + if blueprint == "yeastar": + return "watchdog_dnd.configure" if "dnd" in endpoint and mutating else ("watchdog_dnd.view" if "dnd" in endpoint else "integration.yeastar.configure" if mutating else "integration.yeastar.view") + if blueprint == "logs": + return "watchdog_dnd.view" if "watchdog" in endpoint or endpoint == "logs.index" else "audit.view" + if blueprint in {"ent", "outlook_pages", "outlook_dashboard", "outlook_sync"}: + integration = "ent" if blueprint == "ent" else "outlook" + return f"integration.{integration}.configure" if mutating else f"integration.{integration}.view" + if blueprint == "pronote": + return "integration.pronote.configure" if mutating else "integration.pronote.view" + if blueprint == "admin": + if endpoint.startswith("admin.user") or endpoint in {"admin.list_users", "admin.users"}: + return "user.manage" if mutating else "user.view" + if endpoint.startswith("admin.role") or endpoint == "admin.permissions": + return "role.manage" if mutating else "role.view" + if endpoint == "admin.audit_logs": + return "audit.view" + if endpoint.startswith("admin.settings"): + return "system.configure" if mutating else "system.view" if blueprint in ADMIN_BLUEPRINTS: return "system.admin" if blueprint == "auth": diff --git a/app_new/core/routes/admin.py b/app_new/core/routes/admin.py index 4b03923..b5166d5 100644 --- a/app_new/core/routes/admin.py +++ b/app_new/core/routes/admin.py @@ -14,7 +14,7 @@ from ..models.college import College, Building, Room, RoomType from ..models.company import Company, Service from ..models.rbac import Role, Permission, UserRole, RolePermission, UserPermission from app_new.constants import INTERVENTION_STATUSES, EQUIPMENT_STATUSES, ROLES -from ..authorization import PERMISSIONS_BY_ROLE, ROLE_LABELS, canonical_role +from ..authorization import PERMISSIONS_BY_ROLE, ROLE_LABELS, canonical_role, has_permission, required_permission, PERMISSION_LABELS admin_bp = Blueprint('admin', __name__) @@ -24,7 +24,8 @@ def admin_required(f): from functools import wraps @wraps(f) def decorated_function(*args, **kwargs): - if not current_user.is_authenticated or not current_user.is_admin(): + required = required_permission(request.endpoint, request.method) + if not current_user.is_authenticated or (not current_user.is_admin() and not has_permission(required)): flash('Accès réservé aux administrateurs.', 'danger') return redirect(url_for('dashboard.index')) return f(*args, **kwargs) @@ -187,7 +188,7 @@ def role_permissions(role_id): flash(f'Permissions du rôle « {role.name} » enregistrées.', 'success') return redirect(url_for('admin.roles')) selected = {link.permission_id for link in role.permission_links if link.effect == 'allow'} - return render_template('admin/role_permissions.html', role=role, permissions_list=permissions_list, selected=selected) + return render_template('admin/role_permissions.html', role=role, permissions_list=permissions_list, selected=selected, permission_labels=PERMISSION_LABELS) @admin_bp.route('/roles//archive', methods=['POST']) diff --git a/app_new/templates/admin/role_permissions.html b/app_new/templates/admin/role_permissions.html index a4b822b..7f41b79 100644 --- a/app_new/templates/admin/role_permissions.html +++ b/app_new/templates/admin/role_permissions.html @@ -1 +1,59 @@ -{% extends "base.html" %}{% block title %}Permissions — {{ role.name }}{% endblock %}{% block content %}

Permissions du rôle « {{ role.name }} »

Les cases cochées autorisent l'action. Les permissions sont regroupées par module.

{% set ns = namespace(module='') %}{% for permission in permissions_list %}{% if permission.module != ns.module %}{% if not loop.first %}
{% endif %}{% set ns.module = permission.module %}
{{ permission.module|title }}
{% endif %}
{% if loop.last %}
{% endif %}{% else %}

Aucune permission. Exécutez la migration RBAC.

{% endfor %}Annuler{% endblock %} +{% extends "base.html" %} +{% block title %}Permissions — {{ role.name }}{% endblock %} +{% block content %} +
+

Permissions du rôle « {{ role.name }} »

+

Les sections sont repliées par défaut. Utilisez « Tout sélectionner » pour appliquer toutes les permissions d’un module.

+
+ + {% for module, permissions in permissions_list|groupby('module') %} + {% set section_id = 'permissions-' ~ (module|slugify) %} +
+ + {{ permission_labels.get(module, module|replace('_', ' ')|title) }} ({{ permissions|length }}) + + +
+ {% for permission in permissions %} +
+ +
+ {% endfor %} +
+
+ {% else %} +

Aucune permission. Exécutez la migration RBAC.

+ {% endfor %} + + Annuler +
+
+ + +{% endblock %} diff --git a/migrations/versions/9d28e9f0a1b2_ajouter_permissions_administration.py b/migrations/versions/9d28e9f0a1b2_ajouter_permissions_administration.py new file mode 100644 index 0000000..2246cd7 --- /dev/null +++ b/migrations/versions/9d28e9f0a1b2_ajouter_permissions_administration.py @@ -0,0 +1,20 @@ +"""Ajouter les permissions d'administration et d'intégration détaillées. +Revision ID: 9d28e9f0a1b2 +Revises: 9c17d8e9f0a1 +""" +from alembic import op +import sqlalchemy as sa +from app_new.core.authorization import GRANULAR_PERMISSION_CODES +revision = "9d28e9f0a1b2" +down_revision = "9c17d8e9f0a1" +branch_labels = None +depends_on = None +def upgrade(): + bind = op.get_bind() + for code in sorted(GRANULAR_PERMISSION_CODES): + bits = code.split(".", 1) + module, action = bits[0], bits[1] if len(bits) > 1 else "manage" + bind.execute(sa.text("INSERT INTO permissions (code,name,module,action,description,is_active) SELECT :code,:name,:module,:action,:description,1 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE code=:code)"), {"code": code, "name": f"{module.title()} — {action}", "module": module, "action": action, "description": "Action détaillée configurable"}) +def downgrade(): + pass +