Affiner les permissions d administration
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run

This commit is contained in:
root 2026-08-21 15:48:59 +00:00
parent 12a66dc6da
commit bab3525ad7
4 changed files with 127 additions and 4 deletions

View file

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

View file

@ -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/<int:role_id>/archive', methods=['POST'])

View file

@ -1 +1,59 @@
{% extends "base.html" %}{% block title %}Permissions — {{ role.name }}{% endblock %}{% block content %}<div class="container-fluid"><h1>Permissions du rôle « {{ role.name }} »</h1><p class="text-muted">Les cases cochées autorisent l'action. Les permissions sont regroupées par module.</p><form method="post"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}">{% set ns = namespace(module='') %}{% for permission in permissions_list %}{% if permission.module != ns.module %}{% if not loop.first %}</div></div>{% endif %}{% set ns.module = permission.module %}<div class="card mb-3"><div class="card-header"><strong>{{ permission.module|title }}</strong></div><div class="card-body row g-2">{% endif %}<div class="col-xl-3 col-md-4"><label class="border rounded p-2 d-block"><input type="checkbox" name="permission_ids" value="{{ permission.id }}" {% if permission.id in selected %}checked{% endif %}> <strong>{{ permission.action }}</strong><br><small class="text-muted">{{ permission.name }}</small></label></div>{% if loop.last %}</div></div>{% endif %}{% else %}<p class="text-muted">Aucune permission. Exécutez la migration RBAC.</p>{% endfor %}<button class="btn btn-primary">Enregistrer les permissions</button><a class="btn btn-outline-secondary ms-2" href="{{ url_for('admin.roles') }}">Annuler</a></form></div>{% endblock %}
{% extends "base.html" %}
{% block title %}Permissions — {{ role.name }}{% endblock %}
{% block content %}
<div class="container-fluid">
<h1>Permissions du rôle « {{ role.name }} »</h1>
<p class="text-muted">Les sections sont repliées par défaut. Utilisez « Tout sélectionner » pour appliquer toutes les permissions dun module.</p>
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% for module, permissions in permissions_list|groupby('module') %}
{% set section_id = 'permissions-' ~ (module|slugify) %}
<details class="card mb-3 permission-section">
<summary class="card-header d-flex align-items-center gap-2" style="cursor:pointer;list-style:none">
<span class="me-auto"><i class="bi bi-chevron-right section-chevron"></i> <strong>{{ permission_labels.get(module, module|replace('_', ' ')|title) }}</strong> <small class="text-muted">({{ permissions|length }})</small></span>
<label class="small mb-0" onclick="event.stopPropagation()">
<input type="checkbox" class="section-toggle me-1" data-section="{{ section_id }}"> Tout sélectionner
</label>
</summary>
<div class="card-body row g-2">
{% for permission in permissions %}
<div class="col-xl-3 col-md-4">
<label class="border rounded p-2 d-block h-100">
<input type="checkbox" class="permission-check section-{{ section_id }}" name="permission_ids" value="{{ permission.id }}" {% if permission.id in selected %}checked{% endif %}>
<strong>{{ permission.action }}</strong><br>
<small class="text-muted">{{ permission.name }}</small>
</label>
</div>
{% endfor %}
</div>
</details>
{% else %}
<p class="text-muted">Aucune permission. Exécutez la migration RBAC.</p>
{% endfor %}
<button class="btn btn-primary">Enregistrer les permissions</button>
<a class="btn btn-outline-secondary ms-2" href="{{ url_for('admin.roles') }}">Annuler</a>
</form>
</div>
<style>
.permission-section[open] .section-chevron { transform: rotate(90deg); }
.section-chevron { display:inline-block; transition:transform .15s ease; }
.permission-section > summary::-webkit-details-marker { display:none; }
</style>
<script>
document.querySelectorAll('.section-toggle').forEach(function(toggle) {
var checks = document.querySelectorAll('.' + toggle.dataset.section);
var sync = function() {
var values = Array.from(checks);
toggle.checked = values.length > 0 && values.every(function(check) { return check.checked; });
toggle.indeterminate = values.some(function(check) { return check.checked; }) && !toggle.checked;
};
toggle.addEventListener('change', function(event) {
event.stopPropagation();
checks.forEach(function(check) { check.checked = toggle.checked; });
toggle.indeterminate = false;
});
checks.forEach(function(check) { check.addEventListener('change', sync); });
sync();
});
</script>
{% endblock %}

View file

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