Chapitre 1518 minAvancé
Exercices corrigés type E6 et E7 SLAM : procédures et triggers
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- traiter un cas de sécurité complet comme à l'E7 SLAM ;
- faire évoluer une base en une heure comme à l'E6 SLAM ;
- dérouler un trigger à la main pour trouver son bogue ;
- compléter et corriger du code procédural sur papier.
9 exercices corrigés
Ce chapitre rassemble des exercices corrigés de procédures stockées et de triggers dans la forme des épreuves du BTS SIO SLAM : un cas de sécurité en plusieurs questions comme à l'écrit E7, une évolution de base de données à réaliser en une heure comme à l'E6, une série d'erreurs à trouver et un déroulé à la main. Tous portent sur la base parc avec ses compléments ; les corrigés ont été exécutés sur MySQL 8.4, sur la base remise à zéro au début du chapitre (voir L'essentiel) : fais de même avant de commencer. Pour la méthode de rédaction propre à chaque épreuve, vois les cours Épreuve E7 SLAM et Épreuve E6 SLAM.
Cas type E7 : la traçabilité des habilitations
Contexte. Sorbier Logistique fait auditer la sécurité de son application d'assistance. Le rapport relève deux faiblesses : « les changements de rôle des comptes ne laissent aucune trace » et « rien n'empêche un technicien qui accède à la base de s'attribuer le rôle administrateur ». La table
compte (id_compte, login, mot_de_passe, role, actif, id_employe)et la tablejournal_securite (id_journal, evenement, id_compte, detail, auteur, date_evenement)sont fournies (script complémentaire du chapitre 1). L'application, après avoir authentifié l'utilisateur, exécuteSET @utilisateur_appli = 'login';sur sa connexion.
Exercice 1 sur 9 Question 1 : justifier le choix Réussi À revoir
Le responsable propose de tracer les changements de rôle dans le code PHP de la page d'administration. Justifie en quelques phrases le choix d'un déclencheur à la place.
Voir la réponseMasquer la réponse de l'exercice 1
Un déclencheur s'exécute dans le SGBD à chaque modification de la table compte, quel que soit l'outil qui la modifie : la page d'administration, un autre écran, un script, phpMyAdmin ou un client SQL. Une trace écrite dans le code PHP ne couvrirait que la page d'administration : précisément, le rapport pointe un technicien qui accéderait directement à la base. La règle est écrite une seule fois et ne peut pas être oubliée par un développeur. Enfin, le compte de l'application n'a besoin d'aucun droit d'écriture sur journal_securite : le déclencheur l'alimente avec les droits de son propriétaire, et un attaquant passé par l'application ne peut pas effacer ses traces.
Tu avais trouvé ?
Exercice 2 sur 9 Question 2 : tracer les changements de rôle Réussi À revoir
Écris le déclencheur qui ajoute une ligne dans journal_securite à chaque changement de rôle d'un compte : événement 'role_modifie', le compte concerné, l'ancien et le nouveau rôle dans detail (par exemple demandeur -> technicien), et l'auteur (l'utilisateur de l'application s'il est connu, le compte MySQL sinon).
Voir la réponseMasquer la réponse de l'exercice 2
DROP TRIGGER IF EXISTS trg_compte_journal; -- s'il existe depuis le chapitre 9
DELIMITER //
CREATE TRIGGER trg_compte_journal
AFTER UPDATE ON compte
FOR EACH ROW
BEGIN
IF OLD.role <> NEW.role THEN
INSERT INTO journal_securite (evenement, id_compte, detail, auteur)
VALUES ('role_modifie', NEW.id_compte, CONCAT(OLD.role, ' -> ', NEW.role),
COALESCE(@utilisateur_appli, USER()));
END IF;
END //
DELIMITER ;
AFTER : on ne trace qu'un changement réellement enregistré. Le test OLD.role <> NEW.role évite une trace à chaque modification d'un autre champ (un mot de passe, l'état actif). role est NOT NULL : <> suffit. USER(), et non CURRENT_USER(), qui donnerait le propriétaire du déclencheur.
Tu avais trouvé ?
Exercice 3 sur 9 Question 3 : empêcher l'auto-promotion Réussi À revoir
Écris un déclencheur qui refuse, avec le message « Seul un administrateur peut attribuer le rôle admin », toute modification qui fait passer un compte au rôle admin si l'utilisateur de l'application qui la demande n'est pas lui-même un administrateur actif. Que se passe-t-il pour un UPDATE fait depuis un client SQL, sans la variable ? Est-ce une protection suffisante ?
Voir la réponseMasquer la réponse de l'exercice 3
DELIMITER //
CREATE TRIGGER trg_compte_controle_admin
BEFORE UPDATE ON compte
FOR EACH ROW
BEGIN
IF NEW.role = 'admin' AND OLD.role <> 'admin'
AND NOT EXISTS (SELECT 1 FROM compte
WHERE login = @utilisateur_appli AND role = 'admin' AND actif) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Seul un administrateur peut attribuer le rôle admin';
END IF;
END //
DELIMITER ;
Essais : avec @utilisateur_appli = 'clambert' (technicienne), UPDATE compte SET role = 'admin' WHERE id_compte = 5 est refusé ; avec 'amercier' (administrateur), la promotion d'Hélène Garnier passe et le déclencheur de la question 2 la trace. BEFORE, parce qu'il faut refuser avant l'écriture. Lire la table compte dans un déclencheur de compte est permis ; seule sa modification est interdite (erreur 1442).
Depuis un client SQL sans la variable, @utilisateur_appli vaut NULL, la sous-requête ne trouve rien : la promotion est refusée, même pour root. C'est voulu (l'administrateur passe par l'application), mais la protection a une limite : la variable est déclarative, et quelqu'un qui se connecte directement avec un compte autorisé à modifier compte peut écrire SET @utilisateur_appli = 'amercier'. Elle complète donc les droits MySQL, elle ne les remplace pas : seul le compte de l'application et les administrateurs de la base doivent avoir UPDATE sur compte, et les techniciens n'ont aucun accès direct.
Tu avais trouvé ?
Exercice 4 sur 9 Question 4 : détecter Réussi À revoir
Écris deux requêtes sur journal_securite : (a) les comptes dont le rôle a changé plus d'une fois dans les 7 derniers jours ; (b) les changements de rôle faits hors de l'application. Applique-les après ces instructions :
SET @utilisateur_appli = 'amercier';
UPDATE compte SET role = 'admin' WHERE id_compte = 1;
UPDATE compte SET role = 'responsable' WHERE id_compte = 1;
SET @utilisateur_appli = NULL;
UPDATE compte SET role = 'technicien' WHERE id_compte = 2; -- depuis un client SQL, avec root
Voir la réponseMasquer la réponse de l'exercice 4
-- (a)
SELECT id_compte, COUNT(*) AS changements
FROM journal_securite
WHERE evenement = 'role_modifie'
AND date_evenement >= NOW() - INTERVAL 7 DAY
GROUP BY id_compte
HAVING COUNT(*) > 1;
-- (b) l'auteur est un compte MySQL (« utilisateur@hôte »), pas un login de l'application
SELECT id_journal, id_compte, detail, auteur
FROM journal_securite
WHERE evenement = 'role_modifie'
AND auteur LIKE '%@%';
(a) renvoie le compte 1 avec 2 changements : Hélène Garnier est passée administratrice puis est revenue responsable, un aller-retour qui mérite une question. (b) renvoie la ligne demandeur -> technicien du compte 2, d'auteur root@localhost : un changement fait directement dans la base, à vérifier. Le filtre sur la date est dans le WHERE, le seuil sur le nombre dans le HAVING.
Tu avais trouvé ?
Exercice 5 sur 9 Question 5 : protéger le journal Réussi À revoir
L'application se connecte avec le compte 'helpdesk'@'192.168.21.20', qui a aujourd'hui ALL PRIVILEGES ON parc.*. Écris les instructions qui lui donnent seulement : la lecture des tables ticket, intervention, poste, employe et compte, la modification de compte et de ticket, l'ajout d'interventions, et aucun droit sur journal_securite. Le déclencheur pourra-t-il encore écrire dans le journal ?
Voir la réponseMasquer la réponse de l'exercice 5
REVOKE ALL PRIVILEGES ON parc.* FROM 'helpdesk'@'192.168.21.20';
GRANT SELECT ON parc.ticket TO 'helpdesk'@'192.168.21.20';
GRANT SELECT ON parc.intervention TO 'helpdesk'@'192.168.21.20';
GRANT SELECT ON parc.poste TO 'helpdesk'@'192.168.21.20';
GRANT SELECT ON parc.employe TO 'helpdesk'@'192.168.21.20';
GRANT SELECT, UPDATE ON parc.compte TO 'helpdesk'@'192.168.21.20';
GRANT UPDATE ON parc.ticket TO 'helpdesk'@'192.168.21.20';
GRANT INSERT ON parc.intervention TO 'helpdesk'@'192.168.21.20';
Oui : un déclencheur s'exécute avec les droits de son propriétaire (DEFINER), pas avec ceux du compte qui modifie la table. Il suffit que le propriétaire ait INSERT sur journal_securite et le privilège TRIGGER sur compte (chapitre 11). Le compte helpdesk ne peut ni lire, ni modifier, ni effacer les traces.
Tu avais trouvé ?
Exercice 6 sur 9 Question 6 : la durée de conservation Réussi À revoir
Le journal contient des données personnelles. Combien de temps le garder, où cette durée doit-elle être écrite, et comment l'appliquer automatiquement dans MySQL ?
Voir la réponseMasquer la réponse de l'exercice 6
La durée est fixée par le responsable du traitement selon la finalité (pouvoir enquêter sur un incident) : pour des journaux de ce type, la recommandation de la CNIL de 2021 donne un repère de six mois à un an (quoi journaliser et combien de temps). Elle s'écrit dans le registre des traitements (conformité RGPD) et dans la politique de journalisation. Dans MySQL, un événement planifié l'applique :
CREATE EVENT ev_purge_journal_securite
ON SCHEDULE EVERY 1 DAY STARTS '2026-11-09 03:30:00'
DO DELETE FROM journal_securite WHERE date_evenement < NOW() - INTERVAL 1 YEAR;
Vérifie que le planificateur tourne (event_scheduler à ON, valeur par défaut de MySQL 8.4) et surveille la colonne last_executed d'information_schema.events.
Tu avais trouvé ?
Cas type E6 : rouvrir un ticket, en une heure
Expression des besoins. « Les techniciens ajoutent parfois une intervention sur un ticket déjà résolu, ce qui fausse les statistiques. On veut que ce soit impossible, quel que soit l'outil. Mais un ticket résolu peut réellement revenir (l'écran noir du ticket 10 est revenu) : il faut pouvoir le rouvrir, en gardant la trace de qui l'a rouvert et pourquoi. »
Exercice 7 sur 9 La réalisation Réussi À revoir
Livre : (1) le déclencheur qui refuse une intervention sur un ticket résolu ; (2) une procédure rouvrir_ticket(p_id_ticket, p_motif) qui repasse le ticket « en cours », efface sa date de clôture, ajoute « (rouvert) » à son titre et trace l'opération dans journal_securite ; (3) le script de test ; (4) le script d'annulation.
Voir la réponseMasquer la réponse de l'exercice 7
(1) Le déclencheur. La règle doit s'appliquer « quel que soit l'outil » : c'est un déclencheur, pas un contrôle dans l'application.
DELIMITER //
CREATE TRIGGER trg_intervention_ticket_ouvert
BEFORE INSERT ON intervention
FOR EACH ROW
BEGIN
DECLARE v_statut VARCHAR(10);
SELECT statut INTO v_statut FROM ticket WHERE id_ticket = NEW.id_ticket;
IF v_statut = 'resolu' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Ticket résolu : rouvre-le avant d''ajouter une intervention';
END IF;
END //
DELIMITER ;
(2) La procédure, qui gère sa transaction :
DELIMITER //
CREATE PROCEDURE rouvrir_ticket(IN p_id_ticket INT, IN p_motif VARCHAR(200))
COMMENT 'Gère sa propre transaction. Erreur 1644 : ticket inconnu ou non résolu.'
BEGIN
DECLARE v_statut VARCHAR(10);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
SELECT statut INTO v_statut FROM ticket WHERE id_ticket = p_id_ticket FOR UPDATE;
IF v_statut IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Ticket inconnu';
ELSEIF v_statut <> 'resolu' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Seul un ticket résolu peut être rouvert';
END IF;
UPDATE ticket
SET statut = 'en_cours', date_cloture = NULL,
titre = LEFT(CONCAT(titre, ' (rouvert)'), 100)
WHERE id_ticket = p_id_ticket;
INSERT INTO journal_securite (evenement, id_compte, detail, auteur)
VALUES ('ticket_rouvert', 0, LEFT(CONCAT('ticket ', p_id_ticket, ' : ', p_motif), 255),
COALESCE(@utilisateur_appli, USER()));
COMMIT;
END //
DELIMITER ;
LEFT(…, 100) protège la colonne titre (VARCHAR(100)) : en mode strict, un titre trop long ferait échouer l'UPDATE (erreur 1406). Si le déclencheur de clôture du chapitre 8 existe, il efface déjà la date ; l'écrire ici rend la procédure juste même sans lui.
(3) Le script de test, sur la base fraîche :
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min)
VALUES (10, 11, '2026-11-03 09:00:00', 20);
-- ERROR 1644 (45000): Ticket résolu : rouvre-le avant d'ajouter une intervention
CALL rouvrir_ticket(10, 'Écran noir revenu'); -- réussit
CALL rouvrir_ticket(10, 'deux fois');
-- ERROR 1644 (45000): Seul un ticket résolu peut être rouvert
CALL rouvrir_ticket(99, 'x');
-- ERROR 1644 (45000): Ticket inconnu
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min)
VALUES (10, 11, '2026-11-03 09:00:00', 20); -- acceptée
SELECT titre, statut, date_cloture FROM ticket WHERE id_ticket = 10;
-- Écran noir au démarrage (rouvert) | en_cours | NULL
(4) L'annulation : DROP TRIGGER IF EXISTS trg_intervention_ticket_ouvert; DROP PROCEDURE IF EXISTS rouvrir_ticket;. Les tickets déjà rouverts restent rouverts : annuler le code n'annule pas les données.
À l'entretien, prépare deux réponses : pourquoi un déclencheur pour l'interdiction (la règle vaut pour tous les outils) mais une procédure pour la réouverture (une opération en plusieurs étapes, appelée volontairement, avec un motif) ; et pourquoi FOR UPDATE (deux techniciens qui rouvrent en même temps ne créent pas deux traces).
Tu avais trouvé ?
Trouve l'erreur
Exercice 8 sur 9 Six codes faux Réussi À revoir
Chaque extrait contient une erreur. Trouve-la et donne la correction.
CREATE TRIGGER trg_a BEFORE INSERT ON poste BEGIN SET NEW.marque = UPPER(NEW.marque); END- Dans une procédure :
DECLARE c CURSOR FOR SELECT id_poste FROM poste; DECLARE v_id INT; CREATE TRIGGER trg_b AFTER UPDATE ON ticket FOR EACH ROW SET NEW.titre = TRIM(NEW.titre);- Dans une procédure :
IF v_licences = NULL THEN SET p_message = 'Logiciel libre'; END IF; - Un trigger
AFTER INSERT ON installationqui exécuteUPDATE installation SET date_installation = CURDATE() WHERE id_poste = NEW.id_poste; - Dans une procédure transactionnelle :
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
Voir la réponseMasquer la réponse de l'exercice 8
- Il manque
FOR EACH ROW, obligatoire en MySQL :… ON poste FOR EACH ROW BEGIN … END. - Les variables se déclarent avant les curseurs (erreur 1337) :
DECLARE v_id INT;d'abord. NEWne se modifie que dans un triggerBEFORE(erreur 1362). Il fautBEFORE UPDATE.= NULLn'est jamais vrai : la comparaison vautNULL. Il fautIF v_licences IS NULL THEN.- Un trigger ne peut pas modifier sa propre table (erreur 1442 à l'exécution). Pour fixer la date de la ligne insérée, un trigger
BEFORE INSERTavecSET NEW.date_installation = CURDATE();. - Le gestionnaire annule mais ne relance pas l'erreur : l'appelant croit que l'opération a réussi. Il faut
BEGIN ROLLBACK; RESIGNAL; END.
Tu avais trouvé ?
Dérouler à la main
Exercice 9 sur 9 Trois triggers, cinq instructions Réussi À revoir
On suppose que la base, remise à zéro, ne contient que trois triggers, recréés depuis ton fichier de routines : trg_intervention_prise_en_charge (chapitre 8 : une intervention sur un ticket ouvert le passe « en cours »), trg_ticket_cloture (chapitre 8 : la date de clôture suit le statut) et trg_ticket_audit_statut (chapitre 9 : chaque changement de statut est tracé dans audit_ticket). On exécute :
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min, compte_rendu)
VALUES (12, 11, '2026-10-22 10:00:00', 15, 'Clavier changé'); -- (1)
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min, compte_rendu)
VALUES (12, 11, '2026-10-22 11:00:00', 5, 'Vérification'); -- (2)
UPDATE ticket SET statut = 'resolu' WHERE id_ticket = 12; -- (3)
UPDATE ticket SET priorite = 'haute' WHERE id_ticket = 12; -- (4)
UPDATE ticket SET statut = 'resolu', date_cloture = '2026-01-01' WHERE id_ticket = 12; -- (5)
Pour chaque instruction, dis quels triggers s'exécutent et ce qu'ils font. Combien de lignes contient audit_ticket à la fin ? Que devient l'instruction (5) ?
Voir la réponseMasquer la réponse de l'exercice 9
- L'
INSERTdéclenchetrg_intervention_prise_en_charge, qui passe le ticket 12 deouvertàen_cours. CetUPDATEdéclenche à son tourtrg_ticket_cloture(le statut n'est pas résolu :date_clotureresteNULL) ettrg_ticket_audit_statut(traceouvert → en_cours). - Le trigger de l'intervention s'exécute, mais son
UPDATE … WHERE statut = 'ouvert'ne touche aucune ligne : le ticket est déjà en cours. Aucun trigger deticketne s'exécute, aucune trace. trg_ticket_cloturerenseignedate_clotureavec l'heure courante ;trg_ticket_audit_statuttraceen_cours → resolu.- Les deux triggers de
tickets'exécutent, mais ne font rien : le statut ne change pas. - Le ticket est déjà résolu :
trg_ticket_cloturene fait rien (ni première branche, l'ancien statut étant déjàresolu, ni seconde), et la date fournie, le 1er janvier 2026, est antérieure à l'ouverture du ticket (21 octobre 2026). La contrainteck_ticket_datesrefuse l'instruction :ERROR 3819 (HY000): Check constraint 'ck_ticket_dates' is violated.
audit_ticket contient 2 lignes : ouvert → en_cours et en_cours → resolu. Le ticket 12 finit resolu, en priorité haute, avec la date de clôture de l'instruction (3).
Tu avais trouvé ?