Centralise les rôles et les permissions
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run

This commit is contained in:
root 2026-08-14 18:53:03 +00:00
parent b051930431
commit d3ddfdb974
13 changed files with 367 additions and 200 deletions

View file

@ -55,6 +55,10 @@ def create_app(config_name='default'):
migrate.init_app(app, db)
csrf.init_app(app)
from .core.authorization import enforce_route_permission, has_permission
app.before_request(enforce_route_permission)
app.jinja_env.globals["has_permission"] = has_permission
# Configuration du login manager
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Veuillez vous connecter pour accéder à cette page.'

View file

@ -77,7 +77,10 @@ WORK_TYPES = {
# Rôles utilisateurs
ROLES = {
"tech": "Technicien",
"chef": "Chef de service",
"admin": "Administrateur",
"responsable_gmao": "Responsable GMAO",
"technicien": "Technicien",
"assistant_prevention": "Assistant de prévention",
"demandeur": "Demandeur",
"lecture": "Lecture seule",
}

View file

@ -0,0 +1,150 @@
"""Contrôle d'accès centralisé de l'application.
La politique s'applique à toutes les routes enregistrées. Les décorateurs locaux
restent utiles pour documenter les cas particuliers, mais une route oubliée ne
retombe plus sur le simple fait d'être connecté.
"""
from functools import wraps
from flask import abort, request
from flask_login import current_user
ROLE_ALIASES = {
"super_admin": "admin",
"admin": "admin",
"chef": "responsable_gmao",
"responsable": "responsable_gmao",
"responsable_gmao": "responsable_gmao",
"tech": "technicien",
"technician": "technicien",
"technicien": "technicien",
"assistant_prevention": "assistant_prevention",
"requester": "demandeur",
"user": "demandeur",
"demandeur": "demandeur",
"viewer": "lecture",
"lecture": "lecture",
}
ROLE_LABELS = {
"admin": "Administrateur système",
"responsable_gmao": "Responsable GMAO",
"technicien": "Technicien",
"assistant_prevention": "Assistant de prévention",
"demandeur": "Demandeur",
"lecture": "Lecture seule",
}
PERMISSIONS_BY_ROLE = {
"admin": {"*"},
"responsable_gmao": {
"dashboard.view", "intervention.view", "intervention.create", "intervention.manage",
"patrimoine.view", "patrimoine.manage", "planning.view", "planning.manage",
"stock.view", "stock.manage", "contract.view", "contract.manage",
"prevention.view", "prevention.manage", "export.use",
},
"technicien": {
"dashboard.view", "intervention.view", "intervention.create", "intervention.manage",
"patrimoine.view", "patrimoine.manage", "planning.view", "planning.manage",
"stock.view", "stock.manage", "contract.view", "prevention.view", "export.use",
},
"assistant_prevention": {
"dashboard.view", "intervention.view", "intervention.create", "patrimoine.view",
"planning.view", "prevention.view", "prevention.manage", "export.use",
},
"demandeur": {"dashboard.view", "intervention.view", "intervention.create"},
"lecture": {"dashboard.view", "intervention.view", "patrimoine.view", "planning.view", "contract.view"},
}
PUBLIC_ENDPOINTS = {
"auth.login", "health.health", "static",
"setup_wizard.index", "setup_wizard.api_progress", "setup_wizard.api_save_step",
"setup_wizard.api_complete", "setup_wizard.api_test_ent", "setup_wizard.api_test_pronote",
}
ADMIN_BLUEPRINTS = {
"admin", "ai_config", "gmao_config", "logs", "status", "yeastar",
"outlook_auth", "outlook_dashboard", "outlook_pages", "outlook_sync", "ent", "pronote",
}
PATRIMOINE_BLUEPRINTS = {
"equipments", "equipments_meters", "equipments_documents", "equipments_restrictions",
"equipments_scheduled", "buildings", "zones", "rooms", "room_types", "wizard", "lots",
}
PLANNING_BLUEPRINTS = {"planning", "scheduler", "interventions_planning"}
STOCK_BLUEPRINTS = {"parts", "meters"}
CONTRACT_BLUEPRINTS = {"companies", "contracts", "services"}
PREVENTION_BLUEPRINTS = {"trainings", "constraints"}
def canonical_role(role):
return ROLE_ALIASES.get((role or "").strip().lower(), "lecture")
def has_permission(permission, user=None):
user = user or current_user
if not getattr(user, "is_authenticated", False):
return False
permissions = PERMISSIONS_BY_ROLE.get(canonical_role(user.role), set())
return "*" in permissions or permission in permissions
def permission_required(permission):
def decorator(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not has_permission(permission):
abort(403)
return view(*args, **kwargs)
return wrapped
return decorator
def required_permission(endpoint, method):
"""Déduit la permission minimale d'une route enregistrée."""
if not endpoint:
return None
blueprint = endpoint.split(".", 1)[0]
mutating = method not in {"GET", "HEAD", "OPTIONS"}
if blueprint in ADMIN_BLUEPRINTS:
return "system.admin"
if blueprint == "auth":
return "system.admin" if endpoint not in {"auth.profile", "auth.change_own_password", "auth.logout"} else None
if blueprint == "setup_wizard":
return "system.admin"
if blueprint in PATRIMOINE_BLUEPRINTS:
return "patrimoine.manage" if mutating else "patrimoine.view"
if blueprint in PLANNING_BLUEPRINTS:
return "planning.manage" if mutating else "planning.view"
if blueprint == "interventions":
if endpoint in {"interventions.create", "interventions.create_for_group"}:
return "intervention.create"
return "intervention.manage" if mutating else "intervention.view"
if blueprint in STOCK_BLUEPRINTS:
return "stock.manage" if mutating else "stock.view"
if blueprint in CONTRACT_BLUEPRINTS:
return "contract.manage" if mutating else "contract.view"
if blueprint in PREVENTION_BLUEPRINTS:
return "prevention.manage" if mutating else "prevention.view"
if blueprint == "exports":
return "export.use"
if blueprint in {"dashboard", "notifications", "messagerie", "chatbot"}:
return "dashboard.view"
return "system.admin" if mutating else "dashboard.view"
def enforce_route_permission():
"""Garde global appelé avant chaque requête."""
endpoint = request.endpoint
if endpoint in PUBLIC_ENDPOINTS or endpoint == "static":
return None
# L'API REST applique sa clé ou sa session dans son propre décorateur.
if endpoint and endpoint.startswith("api_v1."):
return None
if not current_user.is_authenticated:
return None # Flask-Login conserve la redirection habituelle des vues protégées.
permission = required_permission(endpoint, request.method)
if permission and not has_permission(permission):
abort(403)
return None

View file

@ -32,9 +32,12 @@ class UserForm(FlaskForm):
full_name = StringField("Nom complet", validators=[DataRequired()])
password = PasswordField("Mot de passe", validators=[Optional(), Length(min=12)])
role = SelectField("Rôle", choices=[
("tech", "Technicien"),
("chef", "Chef de service"),
("admin", "Administrateur"),
("responsable_gmao", "Responsable GMAO"),
("technicien", "Technicien"),
("assistant_prevention", "Assistant de prévention"),
("demandeur", "Demandeur"),
("lecture", "Lecture seule"),
])
is_active = BooleanField("Actif")

View file

@ -18,7 +18,7 @@ class User(UserMixin, db.Model):
email = db.Column(db.String(120), unique=True, nullable=True)
password_hash = db.Column(db.String(256), nullable=False)
full_name = db.Column(db.String(150), nullable=False, default="")
role = db.Column(db.String(20), default="user") # admin, user, viewer
role = db.Column(db.String(32), default="demandeur", nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
last_login = db.Column(db.DateTime, nullable=True)
@ -33,7 +33,22 @@ class User(UserMixin, db.Model):
def is_admin(self):
"""Vérifie si l'utilisateur est admin."""
return self.role == "admin"
from ..authorization import canonical_role
return canonical_role(self.role) == "admin"
@property
def canonical_role(self):
from ..authorization import canonical_role
return canonical_role(self.role)
@property
def role_label(self):
from ..authorization import ROLE_LABELS
return ROLE_LABELS.get(self.canonical_role, self.role or "")
def has_permission(self, permission):
from ..authorization import has_permission
return has_permission(permission, self)
def __repr__(self):
return f"<User {self.username}>"

View file

@ -8,7 +8,8 @@ from ...extensions import db
from ..models.user import User, Staff
from ..models.college import College, Building, Room, RoomType
from ..models.company import Company, Service
from app_new.constants import INTERVENTION_STATUSES, EQUIPMENT_STATUSES
from app_new.constants import INTERVENTION_STATUSES, EQUIPMENT_STATUSES, ROLES
from ..authorization import PERMISSIONS_BY_ROLE, ROLE_LABELS, canonical_role
admin_bp = Blueprint('admin', __name__)
@ -62,7 +63,7 @@ def user_new():
email = (request.form.get('email') or '').strip()
password = request.form.get('password') or ''
password_confirm = request.form.get('password_confirm') or ''
role = request.form.get('role', 'user')
role = request.form.get('role', 'demandeur')
if len(username) < 3:
flash('Le nom dutilisateur doit contenir au moins 3 caractères.', 'danger')
@ -86,7 +87,7 @@ def user_new():
flash('Utilisateur créé avec succès.', 'success')
return redirect(url_for('admin.users'))
return render_template('admin/user_form.html')
return render_template('admin/user_form.html', roles=ROLES)
@admin_bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
@ -97,8 +98,12 @@ def user_edit(user_id):
user = User.query.get_or_404(user_id)
if request.method == 'POST':
role = request.form.get('role')
if role not in ROLES:
flash('Rôle utilisateur invalide.', 'danger')
return render_template('admin/user_form.html', user=user, roles=ROLES), 400
user.email = request.form.get('email')
user.role = request.form.get('role')
user.role = role
user.is_active = request.form.get('is_active') == 'on'
new_password = request.form.get('new_password')
@ -112,7 +117,7 @@ def user_edit(user_id):
flash('Utilisateur modifié avec succès.', 'success')
return redirect(url_for('admin.users'))
return render_template('admin/user_form.html', user=user)
return render_template('admin/user_form.html', user=user, roles=ROLES)
@admin_bp.route('/permissions')
@ -121,43 +126,29 @@ def user_edit(user_id):
def permissions():
"""Liste des permissions."""
from ..models.user import User
from ...constants import ROLES
# Compter les utilisateurs par rôle
roles_permissions = {}
for role_key, role_name in ROLES.items():
roles_permissions[role_key] = User.query.filter_by(role=role_key).all()
# Permissions disponibles (simplifié)
all_permissions = [
'dashboard:view', 'interventions:view', 'interventions:create', 'interventions:edit',
'equipments:view', 'equipments:create', 'equipments:edit',
'planning:view', 'planning:create',
'admin:access', 'users:manage'
]
return render_template('admin/permissions.html',
roles_permissions=roles_permissions,
all_permissions=all_permissions)
@admin_bp.route('/permissions', methods=['POST'])
@login_required
@admin_required
def update_permissions():
"""Mettre à jour les permissions."""
flash('Permissions mises à jour.', 'success')
return redirect(url_for('admin.permissions'))
users_by_role = {role: [] for role in ROLES}
for user in User.query.order_by(User.full_name).all():
users_by_role[canonical_role(user.role)].append(user)
return render_template(
'admin/permissions.html', roles=ROLES, role_labels=ROLE_LABELS,
users_by_role=users_by_role, permissions_by_role=PERMISSIONS_BY_ROLE,
)
@admin_bp.route('/users/<int:id>/permissions', methods=['POST'])
@login_required
@admin_required
def update_user_permissions(id):
"""Mettre à jour les permissions d'un utilisateur."""
"""Change le rôle d'un utilisateur (les permissions sont définies en code)."""
user = User.query.get_or_404(id)
flash(f'Permissions de {user.username} mises à jour.', 'success')
return redirect(url_for('admin.user_permissions', id=id))
role = request.form.get('role')
if role not in ROLES:
flash('Rôle invalide.', 'danger')
return redirect(url_for('admin.permissions'))
user.role = role
db.session.commit()
flash(f'Rôle de {user.username} mis à jour.', 'success')
return redirect(url_for('admin.permissions'))
@admin_bp.route('/users/list')

View file

@ -49,7 +49,7 @@ def logout():
@login_required
def list_users():
"""Liste des utilisateurs."""
if current_user.role != 'admin':
if not current_user.is_admin():
flash("Accès réservé aux administrateurs.", "danger")
return redirect(url_for('dashboard.index'))
@ -61,7 +61,7 @@ def list_users():
@login_required
def create_user():
"""Créer un utilisateur."""
if current_user.role != 'admin':
if not current_user.is_admin():
flash("Accès réservé aux administrateurs.", "danger")
return redirect(url_for('dashboard.index'))
@ -98,7 +98,7 @@ def profile():
@login_required
def edit_user(id):
"""Modifier un utilisateur."""
if current_user.role != 'admin':
if not current_user.is_admin():
flash("Accès réservé aux administrateurs.", "danger")
return redirect(url_for('dashboard.index'))
@ -144,7 +144,7 @@ def change_own_password():
@login_required
def delete_user(id):
"""Supprimer un utilisateur."""
if current_user.role != 'admin':
if not current_user.is_admin():
flash("Accès réservé aux administrateurs.", "danger")
return redirect(url_for('dashboard.index'))
@ -163,7 +163,7 @@ def delete_user(id):
@login_required
def change_user_password(id):
"""Changer le mot de passe d'un utilisateur (admin only)."""
if current_user.role != 'admin':
if not current_user.is_admin():
flash("Accès réservé aux administrateurs.", "danger")
return redirect(url_for('dashboard.index'))

View file

@ -1,161 +1,63 @@
{% extends "base.html" %}
{% block title %}Permissions - GMAO Collège{% endblock %}
{% block title %}Rôles et permissions — GMAO{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1 class="h3"><i class="bi bi-shield-check"></i> Gestion des permissions</h1>
<p class="text-muted">Gérer les accès par rôle et utilisateur</p>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3"><i class="bi bi-shield-check"></i> Rôles et permissions</h1>
<p class="text-muted mb-0">La matrice est centralisée dans l'application. Attribuez ici un rôle à chaque compte.</p>
</div>
<a href="{{ url_for('admin.users') }}" class="btn btn-outline-primary"><i class="bi bi-people"></i> Utilisateurs</a>
</div>
<div class="alert alert-info">
Les permissions ne sont pas modifiables case par case : cela évite les droits invisibles et les écarts entre écrans.
Toute évolution de la matrice est versionnée et testée.
</div>
<div class="row g-3">
{% for role_key, role_name in roles.items() %}
<div class="col-xl-4 col-md-6">
<div class="card h-100">
<div class="card-header d-flex justify-content-between">
<strong>{{ role_name }}</strong>
<span class="badge bg-secondary">{{ users_by_role[role_key]|length }} compte(s)</span>
</div>
<div class="card-body">
<h2 class="h6">Permissions</h2>
{% if '*' in permissions_by_role[role_key] %}
<span class="badge bg-danger">Accès complet</span>
{% else %}
<div class="d-flex flex-wrap gap-1 mb-3">
{% for permission in permissions_by_role[role_key]|sort %}
<span class="badge text-bg-light border">{{ permission }}</span>
{% endfor %}
</div>
{% endif %}
{% if users_by_role[role_key] %}
<hr>
{% for user in users_by_role[role_key] %}
<form method="post" action="{{ url_for('admin.update_user_permissions', id=user.id) }}" class="row g-2 align-items-center mb-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col"><span class="small">{{ user.full_name or user.username }}</span></div>
<div class="col-auto">
<a href="{{ url_for('admin.list_users') }}" class="btn btn-outline-primary">
<i class="bi bi-people"></i> Gestion utilisateurs
</a>
</div>
</div>
<!-- Cartes de synthèse -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body text-center">
<i class="bi bi-person-shield fs-1 text-primary"></i>
<h5 class="mt-2">Administrateurs</h5>
<span class="badge bg-primary">{{ roles_permissions.admin|length }}</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body text-center">
<i class="bi bi-tools fs-1 text-success"></i>
<h5 class="mt-2">Techniciens</h5>
<span class="badge bg-success">{{ roles_permissions.technicien|length }}</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body text-center">
<i class="bi bi-building fs-1 text-warning"></i>
<h5 class="mt-2">Chefs</h5>
<span class="badge bg-warning">{{ roles_permissions.chef|length }}</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body text-center">
<i class="bi bi-person-plus fs-1-info"></i>
<h5 class="mt-2">Demandeurs</h5>
<span class="badge bg-info">{{ roles_permissions.demandeur|length }}</span>
</div>
</div>
</div>
</div>
<!-- Tableau des permissions par rôle -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Permissions par rôle</h5>
</div>
<div class="card-body">
<form method="post" action="{{ url_for('admin.update_permissions') }}" class="mb-4">
<div class="row">
{% for role, permissions in roles_permissions.items() %}
<div class="col-lg-6 mb-4">
<div class="border rounded p-3">
<h6 class="text-uppercase text-muted mb-3">{{ role|title }}</h6>
<div class="row g-2">
{% for perm in all_permissions %}
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="permissions"
value="{{ perm }}"
id="perm_{{ role }}_{{ perm.replace('.', '_') }}"
{% if perm in permissions %}checked{% endif %}>
<label class="form-check-label small" for="perm_{{ role }}_{{ perm.replace('.', '_') }}">
{{ perm|replace(':', ': ')|replace('_', ' ')|capitalize }}
</label>
</div>
</div>
<select name="role" class="form-select form-select-sm" aria-label="Rôle de {{ user.username }}">
{% for candidate, label in roles.items() %}
<option value="{{ candidate }}" {% if candidate == role_key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Enregistrer les modifications
</button>
</div>
<div class="col-auto"><button class="btn btn-sm btn-outline-primary" type="submit">Appliquer</button></div>
</form>
</div>
</div>
<!-- Légende -->
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">Légende des permissions</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Dashboard</h6>
<ul class="list-unstyled small">
<li><code>dashboard:view</code> - Voir le tableau de bord</li>
</ul>
</div>
<div class="col-md-6">
<h6>Interventions</h6>
<ul class="list-unstyled small">
<li><code>code:CREATE_INTERVENTION</code> - Créer</li>
<li><code>code:VIEW_INTERVENTIONS</code> - Voir</li>
<li><code>code:EDIT_INTERVENTIONS</code> - Modifier</li>
<li><code>code:DELETE_INTERVENTIONS</code> - Supprimer</li>
</ul>
</div>
<div class="col-md-6">
<h6>Équipements</h6>
<ul class="list-unstyled small">
<li><code>code:VIEW_EQUIPMENTS</code> - Voir</li>
<li><code>code:CREATE_EQUIPMENTS</code> - Créer</li>
<li><code>code:EDIT_EQUIPMENTS</code> - Modifier</li>
<li><code>code:DELETE_EQUIPMENTS</code> - Supprimer</li>
</ul>
</div>
<div class="col-md-6">
<h6>Planning</h6>
<ul class="list-unstyled small">
<li><code>code:VIEW_PLANNING</code> - Voir</li>
<li><code>code:CREATE_PLANNING</code> - Créer</li>
<li><code>code:EDIT_PLANNING</code> - Modifier</li>
</ul>
</div>
<div class="col-md-6">
<h6>ENT77</h6>
<ul class="list-unstyled small">
<li><code>code:VIEW_ENT</code> - Voir les messages</li>
<li><code>code:CONFIG_ENT</code> - Configurer</li>
<li><code>code:MANAGE_ENT_MESSAGES</code> - Gérer</li>
</ul>
</div>
<div class="col-md-6">
<h6>Administration</h6>
<ul class="list-unstyled small">
<li><code>code:VIEW_USERS</code> - Voir utilisateurs</li>
<li><code>code:CREATE_USERS</code> - Créer</li>
<li><code>code:EDIT_USERS</code> - Modifier</li>
<li><code>code:DELETE_USERS</code> - Supprimer</li>
<li><code>code:VIEW_ADMIN</code> - Voir admin</li>
</ul>
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -18,7 +18,7 @@
<div class="card">
<div class="card-body">
<form method="POST">
{{ csrf_token() }}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="username" class="form-label">Nom d'utilisateur *</label>
@ -41,9 +41,9 @@
<div class="mb-3">
<label for="role" class="form-label">Rôle *</label>
<select class="form-select" id="role" name="role" required>
<option value="user" {{ 'selected' if user and user.role == 'user' else '' }}>Utilisateur</option>
<option value="tech" {{ 'selected' if user and user.role == 'tech' else '' }}>Technicien</option>
<option value="admin" {{ 'selected' if user and user.role == 'admin' else '' }}>Administrateur</option>
{% for role_key, role_name in roles.items() %}
<option value="{{ role_key }}" {{ 'selected' if user and user.canonical_role == role_key else '' }}>{{ role_name }}</option>
{% endfor %}
</select>
</div>

View file

@ -223,7 +223,7 @@
</ul>
</li>
{% if current_user.role == 'admin' %}
{% if current_user.is_admin() %}
<li class="nav-group-label d-lg-none mt-2">Administration</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if '/gmao-config' in request.path or '/outlook' in request.path or '/ent' in request.path or '/pronote' in request.path or '/yeastar' in request.path or '/status' in request.path or '/logs' in request.path or '/auth/users' in request.path or '/setup-wizard' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown">

View file

@ -0,0 +1,38 @@
"""Normalise les rôles utilisateurs.
Revision ID: a2b6c7d8e9f0
Revises: f1a5b6c7d8e9
Create Date: 2026-08-14
"""
from alembic import op
import sqlalchemy as sa
revision = "a2b6c7d8e9f0"
down_revision = "f1a5b6c7d8e9"
branch_labels = None
depends_on = None
def upgrade():
op.alter_column(
"users", "role", existing_type=sa.String(length=20),
type_=sa.String(length=32), nullable=False, server_default="demandeur",
)
op.execute("UPDATE users SET role = 'admin' WHERE role = 'super_admin'")
op.execute("UPDATE users SET role = 'responsable_gmao' WHERE role IN ('chef', 'responsable')")
op.execute("UPDATE users SET role = 'technicien' WHERE role IN ('tech', 'technician')")
op.execute("UPDATE users SET role = 'demandeur' WHERE role IN ('user', 'requester') OR role IS NULL OR role = ''")
op.execute("UPDATE users SET role = 'lecture' WHERE role = 'viewer'")
def downgrade():
op.execute("UPDATE users SET role = 'chef' WHERE role = 'responsable_gmao'")
op.execute("UPDATE users SET role = 'tech' WHERE role IN ('technicien', 'assistant_prevention')")
op.execute("UPDATE users SET role = 'user' WHERE role = 'demandeur'")
op.execute("UPDATE users SET role = 'viewer' WHERE role = 'lecture'")
op.alter_column(
"users", "role", existing_type=sa.String(length=32),
type_=sa.String(length=20), nullable=True, server_default="user",
)

View file

@ -13,7 +13,7 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
email = factory.LazyAttribute(lambda obj: f'{obj.username}@test.local')
password_hash = 'pbkdf2:sha256$fakehash$fakehash' # mot de passe factice
full_name = factory.LazyAttribute(lambda obj: obj.username.replace('_', ' ').title())
role = 'user'
role = 'technicien'
is_active = True

View file

@ -0,0 +1,61 @@
"""Vérifie la matrice de rôles sur de vraies routes Flask."""
from uuid import uuid4
import pytest
from app_new import db
from app_new.core.authorization import canonical_role
from app_new.core.models.user import User
def _login_as(client, app, role):
client.get("/auth/logout")
password = "mot-de-passe-test-solide"
with app.app_context():
suffix = uuid4().hex[:8]
user = User(
username=f"rbac_{role}_{suffix}",
email=f"rbac_{role}_{suffix}@test.local",
full_name=f"Test {role}", role=role, is_active=True,
)
user.set_password(password)
db.session.add(user)
db.session.commit()
username = user.username
response = client.post("/auth/login", data={"username": username, "password": password})
assert response.status_code == 302
@pytest.mark.parametrize(
("legacy, canonical"),
[
("super_admin", "admin"), ("chef", "responsable_gmao"),
("tech", "technicien"), ("user", "demandeur"), ("viewer", "lecture"),
],
)
def test_legacy_roles_are_normalized(legacy, canonical):
assert canonical_role(legacy) == canonical
@pytest.mark.parametrize(
("role, equipment_status, admin_status"),
[
("admin", 200, 200),
("responsable_gmao", 200, 403),
("technicien", 200, 403),
("assistant_prevention", 200, 403),
("demandeur", 403, 403),
("lecture", 200, 403),
],
)
def test_role_access_to_real_pages(client, app, role, equipment_status, admin_status):
_login_as(client, app, role)
assert client.get("/equipments/").status_code == equipment_status
assert client.get("/admin/").status_code == admin_status
@pytest.mark.parametrize("role", ["demandeur", "lecture", "assistant_prevention"])
def test_non_maintainers_cannot_mutate_patrimony(client, app, role):
_login_as(client, app, role)
response = client.post("/equipments/categories/new", data={"name": "Interdit"})
assert response.status_code == 403