Skip to Content

Chapitre 11.5 — La Privacy API : Moodle et le RGPD

Partie 11 : Niveau expert — Chapitre 5/8 Public : dev dont le plugin stocke (ou non) des données personnelles ; il doit être conforme et publiable. Version de référence : Moodle 5.2 (Privacy API, tool_dataprivacy).

⏱️ TL;DR

  • Le RGPD impose : minimisation, droit d’accès (export), droit à l’effacement, transparence. Un LMS stocke des données d’étudiants → conformité obligatoire, pas optionnelle.
  • La Privacy API structure cela côté code : chaque plugin déclare ce qu’il stocke via un provider.
  • Un plugin sans données perso implémente null_provider (il déclare « je ne stocke rien »). Obligatoire même dans ce cas — pour la publication.
  • Un plugin avec données perso implémente le provider complet : get_metadata (quoi), get_contexts_for_userid + export_user_data (export), delete_data_for_all_users_in_context / delete_data_for_user / delete_data_for_users (effacement).
  • Côté admin, tool_dataprivacy traite les data requests (demandes d’export/suppression) des utilisateurs, avec workflow d’approbation, et gère le registre des traitements et les politiques de rétention.
  • Tester le provider (base provider_testcase). Le plugin directory exige un provider (11.8).

🎯 Objectifs

  • Comprendre les obligations RGPD appliquées à un LMS.
  • Implémenter un null_provider (plugin sans données perso).
  • Implémenter un provider complet (métadonnées, export, suppression).
  • Comprendre le traitement des data requests côté admin.
  • Tester le provider et satisfaire le plugin directory.

1. Le RGPD, en pratique, pour un développeur

Le RGPD (et ses équivalents) donne aux personnes des droits sur leurs données. Pour un développeur de plugin, quatre se traduisent en code :

  • Minimisation : ne stockez que les données nécessaires. Chaque champ personnel doit se justifier.
  • Droit d’accès (portabilité) : l’utilisateur peut demander tout ce qu’on sait de lui → votre plugin doit savoir exporter ses données.
  • Droit à l’effacement (« droit à l’oubli ») : l’utilisateur peut demander la suppression → votre plugin doit savoir supprimer ses données.
  • Transparence : déclarer quelles données personnelles vous stockez et pourquoi.

Un LMS manipule des données d’apprenants (mineurs parfois) : la conformité n’est pas un « nice to have ». Et concrètement, le plugin directory refuse un plugin sans provider (11.8). Donc : conformité = obligation légale ET technique.

💡 Pour un dev React : c’est le même RGPD que pour vos apps, mais Moodle vous donne un cadre imposé : au lieu d’écrire ad hoc « export mes données », vous implémentez une interface standard (le provider) que le sous-système tool_dataprivacy orchestre pour tous les plugins d’un coup lors d’une demande utilisateur. Vous branchez votre plugin sur une machinerie de conformité déjà construite.


2. L’architecture de la Privacy API

Chaque plugin déclare une classe \<component>\privacy\provider dans classes/privacy/provider.php, qui implémente des interfaces selon ce qu’il stocke :

Les interfaces clés (namespace core_privacy\local\...) :

  • metadata\null_provider : « je ne stocke aucune donnée personnelle » (+ une raison).
  • metadata\provider : déclare quelles données (via get_metadata).
  • request\plugin\provider : export + suppression (le gros du travail).
  • request\core_userlist_provider : lister les utilisateurs concernés dans un contexte (pour la suppression par contexte).

3. Le null_provider : plugin sans données personnelles

Beaucoup de plugins (un thème, un filtre, un bloc d’affichage) ne stockent aucune donnée personnelle. Ils doivent quand même le déclarer :

<?php // public/theme/maboite/classes/privacy/provider.php namespace theme_maboite\privacy; defined('MOODLE_INTERNAL') || die(); class provider implements \core_privacy\local\metadata\null_provider { /** * Explique pourquoi aucune donnée personnelle n'est stockée. * * @return string clé de langue */ public static function get_reason(): string { return 'privacy:metadata'; // chaîne : "This plugin does not store any personal data." } }

Avec la chaîne de langue :

$string['privacy:metadata'] = 'The MaBoîte theme does not store any personal data.';

C’est le minimum obligatoire pour publier (11.8). Ne pas l’implémenter fait échouer la CI (moodle-plugin-ci) et la review.

⚠️ Piège : croire qu’« un thème/filtre n’a pas besoin de provider ». Faux : il doit implémenter null_provider pour déclarer explicitement qu’il ne stocke rien. L’absence de provider ≠ « pas de données » aux yeux de l’outil ; c’est une omission qui bloque la publication.


4. Le provider complet : local_todolist

Notre local_todolist stocke des tâches par utilisateur (données personnelles). Provider complet :

<?php // public/local/todolist/classes/privacy/provider.php namespace local_todolist\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use context; use context_system; use context_user; defined('MOODLE_INTERNAL') || die(); class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * 1. QUELLES données on stocke (transparence). */ public static function get_metadata(collection $collection): collection { $collection->add_database_table( 'local_todolist', [ 'userid' => 'privacy:metadata:local_todolist:userid', 'name' => 'privacy:metadata:local_todolist:name', ], 'privacy:metadata:local_todolist' ); return $collection; } /** * 2. Dans QUELS contextes cet utilisateur a des données (pour l'export). * Ici, toujours le contexte système (les tâches ne sont pas liées à un cours). */ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new contextlist(); global $DB; if ($DB->record_exists('local_todolist', ['userid' => $userid])) { $contextlist->add_system_context(); } return $contextlist; } /** * 3. Quels UTILISATEURS ont des données dans ce contexte (pour la suppression par contexte). */ public static function get_users_in_context(userlist $userlist): void { $context = $userlist->get_context(); if ($context instanceof context_system) { global $DB; $userids = $DB->get_fieldset_select('local_todolist', 'DISTINCT userid', ''); $userlist->add_users($userids); } } /** * 4. EXPORTER les données de l'utilisateur. */ public static function export_user_data(approved_contextlist $contextlist): void { global $DB, $USER; if (!in_array(CONTEXT_SYSTEM, array_map(fn($c) => $c->contextlevel, $contextlist->get_contexts()))) { return; } $userid = $contextlist->get_user()->id; $tasks = $DB->get_records('local_todolist', ['userid' => $userid]); $data = array_map(fn($t) => [ 'name' => $t->name, 'completed' => $t->completed ? get_string('yes') : get_string('no'), ], array_values($tasks)); writer::with_context(context_system::instance())->export_data( [get_string('pluginname', 'local_todolist')], (object) ['tasks' => $data] ); } /** * 5a. SUPPRIMER pour TOUS les utilisateurs d'un contexte. */ public static function delete_data_for_all_users_in_context(context $context): void { global $DB; if ($context instanceof context_system) { $DB->delete_records('local_todolist', []); } } /** * 5b. SUPPRIMER pour UN utilisateur (sur ses contextes approuvés). */ public static function delete_data_for_user(approved_contextlist $contextlist): void { global $DB; foreach ($contextlist->get_contexts() as $context) { if ($context instanceof context_system) { $DB->delete_records('local_todolist', ['userid' => $contextlist->get_user()->id]); } } } /** * 5c. SUPPRIMER pour une LISTE d'utilisateurs approuvés dans un contexte. */ public static function delete_data_for_users(approved_userlist $userlist): void { global $DB; $context = $userlist->get_context(); if ($context instanceof context_system) { [$insql, $inparams] = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $DB->delete_records_select('local_todolist', "userid $insql", $inparams); } } }

Les cinq responsabilités :

  1. get_metadatadéclare les tables/champs personnels et pourquoi (chaînes privacy:metadata:*, ch. 6.3).
  2. get_contexts_for_userid — dans quels contextes l’utilisateur a des données (pour cibler l’export).
  3. get_users_in_context — quels utilisateurs ont des données dans un contexte (pour la suppression par contexte).
  4. export_user_dataécrit les données via le writer (Moodle assemble un export global tous plugins confondus).
  5. delete_data_for_*supprime, selon le déclencheur : tout un contexte, un utilisateur, ou une liste.

⚠️ Piège : export_user_data ne doit exporter que les données de l’utilisateur concerné, et delete_data_for_user ne supprimer que les siennes (filtre userid). Une erreur ici exporte/supprime les données d’autrui — violation grave. C’est le même principe d’appartenance que l’IDOR (ch. 6.3), avec un enjeu RGPD.

💡 Pour un dev React : voyez le provider comme l’implémentation d’une interface DataSubjectRights que le framework appelle : « exporte ce user », « supprime ce user ». Vous ne gérez pas le workflow (demande, approbation, assemblage du fichier) — vous fournissez les méthodes CRUD-par-utilisateur, tool_dataprivacy orchestre le reste pour tous les plugins.


5. Le traitement des data requests (côté admin)

Le plugin tool_dataprivacy est le sous-système qui orchestre :

  • Un utilisateur (ou un DPO) crée une data request : export ou suppression.
  • La demande passe par un workflow d’approbation (un délégué à la protection des données valide).
  • À l’approbation, Moodle appelle les providers de tous les plugins : export_user_data (assemble un fichier téléchargeable structuré) ou delete_data_for_user (efface partout).
  • Le registre des traitements (data registry) et les politiques de rétention permettent de déclarer les finalités et durées de conservation, et d’automatiser l’expiration des données anciennes.

Votre plugin n’écrit pas ce workflow : il participe en implémentant le provider. C’est la force du modèle — une demande utilisateur balaie tous les plugins conformes.


6. Tester le provider

Moodle fournit une base de test dédiée. On vérifie que get_metadata déclare, que l’export retourne les bonnes données, et que la suppression efface bien :

<?php // public/local/todolist/tests/privacy/provider_test.php namespace local_todolist\privacy; use core_privacy\tests\provider_testcase; final class provider_test extends provider_testcase { public function test_export_and_delete(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); \local_todolist\manager::create($user->id, 'Secret task'); // Export : les contextes doivent inclure le système. $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(1, $contextlist); // Suppression pour l'utilisateur. $approved = new \core_privacy\local\request\approved_contextlist( $user, 'local_todolist', $contextlist->get_contextids() ); provider::delete_data_for_user($approved); global $DB; $this->assertEquals(0, $DB->count_records('local_todolist', ['userid' => $user->id])); } }

Le test valide le contrat de conformité : sans lui, un provider peut sembler correct mais rater l’export ou laisser des données après suppression — un bug RGPD silencieux.


7. Synthèse

La Privacy API traduit le RGPD en code : un null_provider (déclarer qu’on ne stocke rien) ou un provider complet (déclarer, exporter, supprimer par utilisateur/contexte). Le sous-système tool_dataprivacy orchestre les data requests en appelant tous les providers. C’est obligatoire (légalement et pour le plugin directory), à tester (provider_testcase), et à écrire avec la même rigueur d’appartenance que la sécurité (n’exporter/supprimer que les données du bon utilisateur). Conformité assurée, il reste l’innovation (l’IA) et l’exploitation — chapitres suivants.


✏️ Exercices

Exercice 1 — Quel provider ? Pour chaque plugin : (a) un thème ; (b) un plugin qui stocke des notes personnelles d’étudiants ; (c) un filtre de texte ; (d) un bloc affichant des données déjà stockées ailleurs.

✅ Solution

(a) null_provider (un thème ne stocke pas de données perso) ; (b) provider complet (metadata\provider + plugin\provider + core_userlist_provider : déclarer, exporter, supprimer) ; (c) null_provider (un filtre transforme du texte, ne stocke rien) ; (d) null_provider s’il ne fait qu’afficher des données stockées par un autre plugin (c’est ce plugin-là qui les déclare). Dans tous les cas, un provider est requis pour publier — même pour déclarer « rien ».

Exercice 2 — CI qui échoue. moodle-plugin-ci échoue sur votre thème avec une erreur de privacy. Vous êtes sûr qu’il ne stocke rien. Pourquoi, et le correctif ?

✅ Solution

Parce que l’absence de provider est une erreur, pas une preuve d’absence de données : l’outil exige une déclaration explicite. Correctif : implémenter \theme_xxx\privacy\provider implements \core_privacy\local\metadata\null_provider avec get_reason() renvoyant une chaîne (« ne stocke aucune donnée personnelle »). Le null_provider est le minimum obligatoire pour tout plugin, y compris ceux sans données.

Exercice 3 — Export d’autrui. Dans export_user_data, un dev fait $DB->get_records('local_todolist', []) (sans filtre). Quel est le risque RGPD ?

✅ Solution

Il exporterait les tâches de tous les utilisateurs dans l’export de la personne qui a demandé ses données → fuite de données personnelles d’autrui, violation grave du RGPD (et faille de confidentialité). Correctif : filtrer par userid ($contextlist->get_user()->id), n’exporter que les données du demandeur. Même principe d’appartenance que l’IDOR (ch. 6.3), avec enjeu légal.

Exercice 4 — Les cinq responsabilités. Un provider complet implémente get_metadata, export_user_data et delete_data_for_user, mais la CI signale qu’il manque des méthodes. Lesquelles et pourquoi ?

✅ Solution

Il manque probablement get_contexts_for_userid (localiser les données de l’utilisateur pour l’export), get_users_in_context + delete_data_for_users (interface core_userlist_provider, pour la suppression par liste d’utilisateurs dans un contexte) et delete_data_for_all_users_in_context (suppression de tout un contexte, ex. suppression d’un cours). Un provider complet doit gérer tous les déclencheurs de suppression (par contexte, par utilisateur, par liste) et la localisation des contextes — pas seulement delete_data_for_user.

Exercice 5 — Tester la suppression. Écrivez l’assertion clé d’un test qui vérifie que delete_data_for_user efface bien les données.

✅ Solution

Après avoir créé des tâches pour l’utilisateur et appelé provider::delete_data_for_user($approvedContextlist), vérifier qu’il ne reste rien :

$this->assertEquals(0, $DB->count_records('local_todolist', ['userid' => $user->id]));

Idéalement, vérifier aussi que les données d’un autre utilisateur (créées en fixture) sont intactes (pour attraper une suppression trop large). Le test valide le contrat de conformité : export/suppression corrects et bornés au bon utilisateur.


🧠 Quiz de révision

Q1. Quels droits RGPD se traduisent en code dans un plugin ?

Réponse

La minimisation (ne stocker que le nécessaire), le droit d’accès/export, le droit à l’effacement, et la transparence (déclarer quelles données et pourquoi). La Privacy API structure ces obligations via un provider.

Q2. Que fait un null_provider et pourquoi est-il obligatoire ?

Réponse

Il déclare explicitement que le plugin ne stocke aucune donnée personnelle (avec get_reason()). Obligatoire car l’absence de provider est traitée comme une omission qui fait échouer la CI et la review du plugin directory — même un thème/filtre doit le fournir.

Q3. Quelles sont les responsabilités d’un provider complet ?

Réponse

get_metadata (déclarer quoi/pourquoi), get_contexts_for_userid (localiser les données pour l’export), get_users_in_context (utilisateurs concernés), export_user_data (exporter via le writer), et delete_data_for_all_users_in_context / delete_data_for_user / delete_data_for_users (effacer selon le déclencheur). Toujours borné au bon utilisateur.

Q4. Qui orchestre les demandes d’export/suppression ?

Réponse

Le sous-système tool_dataprivacy : il gère les data requests, le workflow d’approbation, appelle les providers de tous les plugins, assemble l’export ou effectue la suppression, et gère le registre des traitements et les politiques de rétention. Le plugin participe via son provider.

Q5. Pourquoi tester le provider, et quel est le piège principal à couvrir ?

Réponse

Parce qu’un provider peut sembler correct mais rater l’export ou laisser des données après suppression (bug RGPD silencieux). Le piège à couvrir : n’exporter/supprimer que les données du bon utilisateur (filtre userid) — vérifier que celles d’autrui restent intactes.

Chapitre suivant : 06 — Le sous-système IA (core_ai) — l’architecture providers / actions / placements du sous-système d’IA de Moodle, la configuration admin et le consentement, le développement d’un provider custom branchant l’API Claude d’Anthropic (aiprovider_), un placement custom, les bonnes pratiques (coûts, journalisation, confidentialité des données élèves), et les différences 4.5 → 5.2.