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 module in ("dashboard", "patrimoine", "intervention", "planning", "stock", "contract", "prevention", "user", "system")
for action in ("view", "create", "edit", "delete", "validate", "reject", "export", "configure", "manage") 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): def canonical_role(role):
@ -160,6 +184,26 @@ def required_permission(endpoint, method):
action = "validate" action = "validate"
return f"stock.{action}" 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: if blueprint in ADMIN_BLUEPRINTS:
return "system.admin" return "system.admin"
if blueprint == "auth": 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.company import Company, Service
from ..models.rbac import Role, Permission, UserRole, RolePermission, UserPermission from ..models.rbac import Role, Permission, UserRole, RolePermission, UserPermission
from app_new.constants import INTERVENTION_STATUSES, EQUIPMENT_STATUSES, ROLES 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__) admin_bp = Blueprint('admin', __name__)
@ -24,7 +24,8 @@ def admin_required(f):
from functools import wraps from functools import wraps
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): 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') flash('Accès réservé aux administrateurs.', 'danger')
return redirect(url_for('dashboard.index')) return redirect(url_for('dashboard.index'))
return f(*args, **kwargs) return f(*args, **kwargs)
@ -187,7 +188,7 @@ def role_permissions(role_id):
flash(f'Permissions du rôle « {role.name} » enregistrées.', 'success') flash(f'Permissions du rôle « {role.name} » enregistrées.', 'success')
return redirect(url_for('admin.roles')) return redirect(url_for('admin.roles'))
selected = {link.permission_id for link in role.permission_links if link.effect == 'allow'} 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']) @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