"""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é à l’administration 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 à l’application','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 à l’application', 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