gmao/migrations/versions/9f4a1b2c3d5e_separer_super_admin_et_admin.py
root 20362fb31e
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Renforcer le RBAC et séparer les administrateurs
2026-08-21 18:10:29 +00:00

119 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Sépare le super administrateur du rôle administrateur configurable.
La migration ne supprime aucune donnée. Les comptes existants reçoivent
explicitement ``super_admin`` afin de garantir la continuité d'accès.
"""
from alembic import op
import sqlalchemy as sa
revision = "9f4a1b2c3d5e"
down_revision = "9e39f0a1b2c3"
branch_labels = None
depends_on = None
PERMISSIONS = {
"dashboard.view", "intervention.view", "intervention.create", "intervention.edit",
"intervention.assign", "intervention.change_status", "intervention.close", "intervention.reject",
"intervention.delete", "intervention.postpone", "intervention.comment", "patrimoine.view",
"patrimoine.create", "patrimoine.edit", "patrimoine.move", "patrimoine.change_quantity",
"patrimoine.retire", "patrimoine.delete", "planning.view", "planning.create", "planning.edit",
"planning.delete", "planning.manage", "stock.view", "stock.edit", "stock.receive", "stock.issue",
"stock.transfer", "stock.inventory", "stock.adjust", "stock.destroy", "stock.transvasement",
"stock.dilute", "stock.configure", "contract.view", "contract.manage", "prevention.view",
"prevention.manage", "export.use", "housing.private", "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", "system.admin",
}
LABELS = {
"dashboard.view": ("Consulter le tableau de bord", "Permet d'accéder au tableau de bord général de la GMAO."),
"intervention.view": ("Consulter les interventions", "Permet de consulter la liste et le détail des interventions."),
"intervention.create": ("Créer une intervention", "Permet d'enregistrer une nouvelle intervention dans la GMAO."),
"intervention.edit": ("Modifier une intervention", "Permet de modifier une intervention existante."),
"intervention.assign": ("Affecter une intervention", "Permet d'affecter une intervention à un agent ou une entreprise."),
"intervention.change_status": ("Changer le statut d'une intervention", "Permet de faire évoluer le statut d'une intervention selon son workflow."),
"intervention.close": ("Clôturer une intervention", "Permet de clôturer une intervention terminée et d'enregistrer son état final."),
"intervention.reject": ("Refuser une intervention", "Permet de refuser une demande en enregistrant un motif."),
"patrimoine.view": ("Consulter le patrimoine", "Permet de consulter les bâtiments, zones, locaux et équipements."),
"patrimoine.edit": ("Modifier le patrimoine", "Permet de modifier les informations des éléments du patrimoine."),
"patrimoine.move": ("Déplacer des équipements", "Permet de déplacer des équipements entre les locaux."),
"stock.view": ("Consulter les stocks", "Permet de consulter les produits, lots, quantités et emplacements."),
"stock.receive": ("Réceptionner des produits", "Permet d'enregistrer l'entrée en stock de produits et leurs lots."),
"stock.issue": ("Sortir des produits du stock", "Permet d'enregistrer une sortie de produits vers un agent ou pour utilisation."),
"stock.transfer": ("Transférer du stock", "Permet de déplacer une quantité entre emplacements."),
"stock.inventory": ("Réaliser un inventaire", "Permet d'enregistrer un comptage physique et de traiter les écarts."),
"stock.dilute": ("Préparer une dilution", "Permet d'enregistrer la préparation d'un produit dilué."),
"system.configure": ("Modifier la configuration générale", "Permet de modifier les paramètres généraux de l'application."),
"user.manage": ("Gérer les utilisateurs", "Permet de créer, modifier, désactiver et administrer les comptes."),
"role.manage": ("Gérer les rôles et permissions", "Permet de créer et modifier les rôles et leurs permissions."),
}
def _label(code):
if code in LABELS:
return LABELS[code]
module, _, action = code.partition(".")
return (f"{action.replace('_', ' ').capitalize()}{module.replace('_', ' ').capitalize()}",
f"Permet d'effectuer l'action « {action.replace('_', ' ')} » dans le domaine concerné.")
def upgrade():
bind = op.get_bind()
bind.execute(sa.text("""INSERT INTO roles (slug,name,description,is_system,is_active,created_at,updated_at)
SELECT 'super_admin','Super administrateur','Rôle système protégé donnant un accès complet à l'application.',1,1,NOW(),NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM roles WHERE slug='super_admin')"""))
bind.execute(sa.text("""UPDATE roles SET name='Super administrateur', description='Rôle système protégé donnant un accès complet à l'application.', is_system=1, is_active=1 WHERE slug='super_admin'"""))
bind.execute(sa.text("""UPDATE roles SET name='Administrateur', description='Rôle système destiné à ladministration courante de la GMAO. Ses permissions sont configurables.' WHERE slug='admin'"""))
role_text = {
'responsable_gmao': ('Responsable GMAO', 'Rôle système destiné à la supervision et à la gestion générale de la GMAO.'),
'technicien': ('Technicien', 'Rôle système destiné aux opérations de maintenance.'),
'assistant_prevention': ('Assistant de prévention', 'Rôle système donnant accès aux fonctions liées à la prévention.'),
'demandeur': ('Demandeur', 'Rôle système destiné aux utilisateurs pouvant créer et suivre des demandes.'),
'lecture': ('Lecture seule', 'Rôle système destiné aux utilisateurs autorisés uniquement à consulter certaines informations.'),
}
for slug, (name, description) in role_text.items():
bind.execute(sa.text("UPDATE roles SET name=:name, description=:description WHERE slug=:slug"), dict(slug=slug, name=name, description=description))
bind.execute(sa.text("""INSERT INTO permissions (code,name,module,action,description,is_active)
SELECT '*','Accès complet à lapplication','system','all','Accès complet réservé au super administrateur.',1
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE code='*')"""))
bind.execute(sa.text("UPDATE permissions SET name='Accès complet à lapplication', description='Accès complet réservé au super administrateur.', is_active=1 WHERE code='*'"))
for code in sorted(PERMISSIONS):
name, description = _label(code)
module, _, action = code.partition('.')
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)"""), dict(code=code, name=name, module=module, action=action, description=description))
bind.execute(sa.text("UPDATE permissions SET name=:name, description=:description, module=:module, action=:action, is_active=1 WHERE code=:code"), dict(code=code, name=name, module=module, action=action, description=description))
# Les permissions historiques conservées reçoivent elles aussi un
# intitulé humain, sans modifier leur code utilisé par le runtime.
for row in bind.execute(sa.text("SELECT code,module,action FROM permissions WHERE code <> '*'" )).all():
name, description = _label(row.code)
bind.execute(sa.text("UPDATE permissions SET name=:name, description=:description WHERE code=:code"), dict(code=row.code, name=name, description=description))
# Le wildcard ne doit plus appartenir à admin.
bind.execute(sa.text("DELETE rp FROM role_permissions rp JOIN roles r ON r.id=rp.role_id JOIN permissions p ON p.id=rp.permission_id WHERE r.slug='admin' AND p.code='*'"))
bind.execute(sa.text("""INSERT INTO role_permissions (role_id,permission_id,effect)
SELECT r.id,p.id,'allow' FROM roles r JOIN permissions p
WHERE r.slug='super_admin' AND p.code='*'
AND NOT EXISTS (SELECT 1 FROM role_permissions rp WHERE rp.role_id=r.id AND rp.permission_id=p.id)"""))
bind.execute(sa.text("""DELETE rp FROM role_permissions rp JOIN roles r ON r.id=rp.role_id
JOIN permissions p ON p.id=rp.permission_id
WHERE r.slug='admin' AND p.code IN ('intervention.manage','patrimoine.manage','stock.manage')"""))
# Admin reçoit les permissions applicatives explicites (dont les anciennes
# permissions conservées en base), sans le wildcard.
bind.execute(sa.text("""INSERT INTO role_permissions (role_id,permission_id,effect)
SELECT r.id,p.id,'allow' FROM roles r CROSS JOIN permissions p
WHERE r.slug='admin' AND p.code <> '*' AND p.is_active=1
AND NOT EXISTS (SELECT 1 FROM role_permissions rp WHERE rp.role_id=r.id AND rp.permission_id=p.id)"""))
# Continuité d'accès : tous les comptes existants reçoivent le rôle protégé.
bind.execute(sa.text("""INSERT INTO user_roles (user_id,role_id,assigned_at)
SELECT u.id,r.id,NOW() FROM users u CROSS JOIN roles r WHERE r.slug='super_admin'
AND NOT EXISTS (SELECT 1 FROM user_roles ur WHERE ur.user_id=u.id AND ur.role_id=r.id)"""))
def downgrade():
# Aucun lien utilisateur ni permission ne doit être supprimé par rollback.
pass