ajoute le dossier WebDAV de l'élève (bloc C)

Dossier personnel de l'élève monté comme lecteur réseau : rclone
sert le dossier local, l'authentification est déléguée au portail
via auth_request Nginx (mêmes identifiants que la connexion), sans
nouveau système de comptes. Nettoyé par la purge RGPD comme les
autres données d'élève.

Corrige au passage un vrai bug d'isolation trouvé en testant avec
deux comptes jetables : rewrite injectait la variable posée par
auth_request_set, mais rewrite s'exécute dans une phase Nginx
antérieure à cette variable, donc toujours vide - un élève pouvait
lire le fichier d'un autre en devinant son nom. Corrigé par une
location à regex nommée + proxy_pass. Détails : ADR 0008.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-14 13:04:40 +02:00
parent cdf79d2823
commit e549f03e1b
19 changed files with 553 additions and 8 deletions

View file

@ -0,0 +1,82 @@
# ADR 0008 — WebDAV de l'élève : authentification déléguée (Nginx auth_request) + rclone
- **Date** : 2026-10-14
- **Statut** : accepté
## Contexte
Bloc C du CLAUDE.md : « dossier de l'élève montable comme lecteur réseau. »
Besoin : chaque élève a un dossier personnel, accessible en WebDAV (Windows/
macOS/Linux savent tous monter un lecteur réseau WebDAV nativement), avec les
mêmes identifiants que la connexion, et **jamais** accès au dossier d'un
autre élève.
## Décisions
**`rclone serve webdav`, pas un module WebDAV Nginx/Apache.** rclone sait
servir aussi bien un dossier local qu'un bucket S3 avec la même commande —
bascule possible plus tard vers Hetzner Object Storage sans rien changer à
l'architecture d'authentification. Empreinte mémoire très faible (~20 Mo).
**Authentification déléguée au portail via `auth_request` (Nginx), pas une
authentification propre à rclone.** rclone n'a pas de notion de comptes
multiples ; Nginx vérifie les identifiants auprès du portail (même logique
que la connexion, `Auth::guard('eleve')->once(...)`) via une sous-requête
interne (`WebdavAuthController`), puis limite la suite de la requête au seul
sous-dossier de l'élève authentifié.
**Un seul process rclone pour tous les élèves, pas un par élève.** Chaque
sous-dossier (`storage/app/webdav/<id élève>/`) est injecté dans le chemin
transmis à rclone via `proxy_pass http://127.0.0.1:.../$eleve_dossier/$chemin`
— $eleve_dossier vient de l'en-tête `X-Eleve-Dossier` posé par
`WebdavAuthController` après vérification des identifiants.
## Deux bugs trouvés en vérifiant en réel (pas en local, Nginx n'a pas de test unitaire)
**1. `rewrite` ne voit jamais la variable posée par `auth_request_set`.**
Nginx exécute les directives `rewrite` dans la phase *rewrite*, qui a lieu
**avant** la phase *access* où tourne `auth_request`/`auth_request_set` —
même écrites après dans le fichier, textuellement. Une première version
utilisait `rewrite ^/webdav/(.*)$ /$eleve_dossier/$1 break;` : `$eleve_dossier`
y était toujours vide, donc tous les fichiers de tous les élèves atterrissaient
à la racine commune du dossier WebDAV — **un élève pouvait lire le fichier
d'un autre simplement en devinant son nom.** Jamais vu en local (pas de test
automatisé qui exerce une vraie pile Nginx+rclone) : découvert en testant
avec deux comptes élèves jetables en vrai sur le serveur.
Corrigé : une `location` avec une regex à groupe nommé
(`location ~ ^/webdav/(?<chemin_webdav>.*)$`) et `proxy_pass` avec la
variable directement dans l'URL — `proxy_pass` construit sa requête dans une
phase plus tardive, où `$eleve_dossier` est déjà posée.
**2. Le cache de répertoires de rclone (`--dir-cache-time`, 5 min par
défaut) ne voit pas les dossiers créés par Laravel.** Chaque dossier élève
est créé par PHP (`Storage::disk('webdav')->makeDirectory(...)`), en dehors
de rclone — qui le découvre seulement à la prochaine actualisation de son
cache de liste. Un élève dont le dossier venait d'être créé pouvait recevoir
une 404 pendant plusieurs minutes. Corrigé par `--dir-cache-time 1s` dans le
service systemd (`infra`/`ansible/roles/portail/templates/rclone-webdav.service.j2`).
`--vfs-cache-mode off` (déjà en place, pour ne jamais garder de contenu de
fichier en cache) ne couvre pas ce cache de répertoires : deux réglages
distincts.
**3. `SCRIPT_NAME`/`REQUEST_URI` ne sont pas fiables dans une sous-requête
`auth_request` quand on construit son propre appel FastCGI.** `$request_uri`
dans la sous-requête reflétait la requête d'origine (`/webdav/…`), pas l'URI
de la sous-requête elle-même (`/interne/webdav-auth`) : Laravel routait donc
sur le mauvais chemin et répondait 404 à chaque appel. Comme cette location
a une URI toujours identique (`= /interne/webdav-auth`), la solution a été
d'écrire `SCRIPT_FILENAME`, `SCRIPT_NAME` et `REQUEST_URI` en dur dans les
paramètres FastCGI plutôt que de les déduire de variables Nginx.
## Conséquences
- Testé en réel avec trois comptes élèves jetables : upload, lecture,
PROPFIND, isolation stricte entre élèves (un 404 propre, jamais le
contenu d'un autre), dossier d'un nouvel élève utilisable immédiatement.
Comptes et fichiers de test supprimés ensuite.
- Cette classe de bug (interaction entre phases Nginx, cache rclone) n'est
détectable qu'en testant la vraie pile déployée — aucun test Pest ne
l'aurait vue, puisque les tests appellent le contrôleur Laravel
directement, sans passer par Nginx ni rclone.
- Reste en dette : pas de quota de stockage par élève, dossier local
seulement (pas encore sur Hetzner Object Storage).

View file

@ -39,3 +39,4 @@ retirer la ligne (et le noter dans le journal) quand c'est fait.
| **Serveur réel : 31 Go de RAM et 230 Go RAID 1, pas 64 Go / 2×500 Go comme écrit dans le CLAUDE.md (section 3)** | Constaté en vérifiant l'état réel avant la démo Ceph (`free -h`, `lsblk`). Change la vraie marge de capacité (hot pool + OpenStack + portail + tout chantier futur) : à corriger dans le CLAUDE.md, ou à traiter avec une vraie mise à niveau matérielle si le pilote (semaine 8) montre que ça manque. | Avant le pilote si la capacité manque | 2026-10-14 |
| Ceph : démo mono-serveur seulement (1 OSD sur fichier épars, pas de réplication, pas branché à Cinder/Glance) | `ceph_pool_default_size=1` et un seul OSD acceptés uniquement parce que c'est une démo de compréhension, sans donnée réelle. Le vrai cluster (plusieurs OSD, réplication, intégré à Cinder) attend le 2ᵉ serveur du CLAUDE.md. Voir [ADR 0007](adr/0007-ceph-demo-mono-serveur.md). | 2ᵉ serveur (bloc C) | 2026-10-14 |
| Ceph : bootstrap fait à la main, pas encore rejouable par Ansible | Comme Kolla à l'étape 2 : acceptable pour une démo, à automatiser si ce cluster doit durer. Le fichier systemd du loop device est suivi dans `infra/ceph/` mais déployé à la main. | Si la démo devient durable | 2026-10-14 |
| WebDAV de l'élève : stockage local, pas S3 ; pas de quota par élève | Dossier local (`storage/app/webdav/`), partage le disque serré avec OpenStack et le portail — pas de limite de taille par élève. rclone sait servir un backend S3 sans changement d'architecture (voir [ADR 0008](adr/0008-webdav-eleve-auth-request.md)) : à basculer en même temps que le bucket S3 des devoirs (`docs/runbooks/stockage-s3.md`), et à ajouter un quota si l'usage réel le demande. | Quand le bucket S3 existe / si l'usage le demande | 2026-10-14 |

View file

@ -1199,3 +1199,55 @@ Deux leçons distinctes : d'abord, ne jamais faire confiance à la documentation
**Suite**
Prochain chantier du bloc C, annoncé par Adam : WebDAV, puis ElasticSearch.
## 2026-10-14 — WebDAV de l'élève (bloc C)
Deuxième chantier du bloc C : dossier personnel de l'élève, monté comme
lecteur réseau. Deux bugs réels trouvés et corrigés en vérifiant en vrai sur
le serveur, détaillés dans [ADR 0008](adr/0008-webdav-eleve-auth-request.md).
**Fait**
- `rclone serve webdav` (20 Mo de RAM) sert le dossier local de chaque élève (`storage/app/webdav/<id>/`), accessible en `https://syllabuslab.fr/webdav/` avec les identifiants de connexion habituels.
- L'authentification est déléguée au portail via `auth_request` de Nginx (`WebdavAuthController`, sous-requête interne, jamais appelable depuis l'extérieur) — pas de nouveau système de comptes.
- Page « Mes fichiers » (menu du compte, élève seulement) : explique comment monter le lecteur sous Windows/macOS/Linux.
- Dossier WebDAV de l'élève supprimé automatiquement lors de la purge RGPD d'une classe archivée (comme pour le stockage S3 des devoirs).
**Bug grave trouvé en testant en réel (pas en local)**
Première version : un `rewrite` injectait le sous-dossier de l'élève
(posé par `auth_request_set` après vérification des identifiants) dans le
chemin transmis à rclone. Testé avec deux comptes élèves jetables : le
fichier déposé par le premier atterrissait à la racine du dossier WebDAV
commun, et **le second élève pouvait le lire en devinant juste son nom** —
un élève aurait pu accéder aux fichiers d'un autre. Cause : `rewrite`
s'exécute dans une phase Nginx antérieure à celle où `auth_request_set` pose
sa variable, qui était donc toujours vide au moment du `rewrite`, même
écrite après dans le fichier. Corrigé avec une `location` à regex nommée et
`proxy_pass` construisant l'URL directement (phase plus tardive, la
variable y est déjà posée) — revérifié avec trois comptes élèves jetables,
isolation stricte confirmée (404 propre, jamais le contenu d'un autre),
comptes supprimés ensuite.
Deuxième bug, plus bénin : le cache de répertoires de rclone (distinct du
cache de contenu de fichier, déjà désactivé) ne voyait pas un dossier élève
tout juste créé par Laravel — 404 pendant que le cache n'avait pas encore
rafraîchi sa liste. Corrigé en réduisant ce cache à 1 seconde (volume
d'élèves trop faible pour que ça coûte quoi que ce soit).
**Comprendre**
Aucun test Pest n'aurait pu voir ni l'un ni l'autre bug : les tests
appellent `WebdavAuthController` directement, sans jamais passer par Nginx
ni par rclone — la pile complète (Nginx → auth_request → PHP-FPM → rclone →
disque) n'existe qu'en production. Pour ce genre de chantier (plusieurs
briques d'infra qui communiquent entre elles), la vérification en réel avec
de vraies requêtes HTTP sur deux comptes différents n'est pas une étape de
confort après coup : c'est la seule façon de voir ce bug-là. La leçon
« rewrite s'exécute avant auth_request_set » est aussi le genre de piège
d'ordre des phases Nginx qui ne saute pas aux yeux en relisant le fichier
de haut en bas — une bonne question à savoir expliquer en entretien SRE.
**Suite**
Prochain chantier du bloc C : ElasticSearch.

67
docs/runbooks/webdav.md Normal file
View file

@ -0,0 +1,67 @@
# Runbook — WebDAV de l'élève
Dossier personnel de l'élève, monté comme lecteur réseau (bloc C du
CLAUDE.md). Détail des choix et des bugs trouvés en vérifiant en réel :
[ADR 0008](../adr/0008-webdav-eleve-auth-request.md).
## Où sont les choses
| Élément | Emplacement |
|---|---|
| Fichiers des élèves | `/var/www/portail/storage/app/webdav/<id élève>/` (jamais versionné, jamais synchronisé par un déploiement) |
| Service WebDAV | `rclone-webdav.service` (systemd), écoute en local seulement sur `127.0.0.1:8765` |
| Authentification | `App\Http\Controllers\WebdavAuthController`, sous-requête Nginx `/interne/webdav-auth` (jamais appelable de l'extérieur) |
| Config Nginx | bloc `/webdav/` dans `ansible/roles/portail/templates/nginx-portail.conf.j2` |
Adresse pour l'élève : `https://syllabuslab.fr/webdav/` (voir « Mes fichiers »
dans le menu du compte). Identifiants : les mêmes que la connexion.
## Vérifier que ça fonctionne
```
systemctl is-active rclone-webdav
curl -s -o /dev/null -w "%{http_code}\n" https://syllabuslab.fr/webdav/ # 401 attendu sans identifiants
```
Avec un compte élève jetable (créé et supprimé ensuite via tinker, jamais un
compte réel) :
```
curl -u IDENTIFIANT:CODE -X PROPFIND -H "Depth: 1" https://syllabuslab.fr/webdav/
curl -u IDENTIFIANT:CODE -T fichier.txt https://syllabuslab.fr/webdav/fichier.txt
```
## Pourquoi RAM et disque sont serrés
Comme pour Ceph (voir `docs/runbooks/ceph.md`) : ce serveur a réellement
31 Go de RAM et 230 Go en RAID 1, pas 64 Go / 2×500 Go comme écrit dans le
CLAUDE.md. rclone utilise ~20 Mo, négligeable — mais chaque fichier déposé
par un élève prend de l'espace disque réel, partagé avec OpenStack et le
portail. Pas de quota par élève pour l'instant (voir `docs/dette.md`).
## Nettoyer entièrement (si besoin)
```
ssh paillasse
sudo systemctl disable --now rclone-webdav.service
sudo rm /etc/systemd/system/rclone-webdav.service
sudo rm -rf /var/www/portail/storage/app/webdav
sudo systemctl daemon-reload
```
Puis retirer le bloc `/webdav/` et `/interne/webdav-auth` du vhost (relancer
`ansible-playbook portail.yml` après avoir enlevé `09-webdav.yml` du
`main.yml` du rôle).
## Écueils rencontrés (pour ne pas les redécouvrir)
- **Jamais `rewrite` pour injecter une variable posée par `auth_request_set`**
dans le chemin proxié — `rewrite` s'exécute avant que la variable existe.
Utiliser une `location` à regex nommée + `proxy_pass` avec la variable
dans l'URL.
- **`--vfs-cache-mode off` ne désactive pas le cache de répertoires de
rclone** (`--dir-cache-time`, 5 min par défaut) : un dossier créé par
Laravel reste invisible de rclone jusqu'à l'expiration du cache, sauf à
mettre `--dir-cache-time` très bas (1 s ici, le volume est faible).
- **`SCRIPT_NAME`/`REQUEST_URI` ne sont pas fiables** dans une sous-requête
`auth_request` qui fait son propre appel FastCGI : les écrire en dur
quand l'URI de la location est fixe.