Affiner les permissions d administration
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
This commit is contained in:
parent
12a66dc6da
commit
bab3525ad7
4 changed files with 127 additions and 4 deletions
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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 d’un 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 %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
Loading…
Reference in a new issue