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:
parent
cdf79d2823
commit
e549f03e1b
19 changed files with 553 additions and 8 deletions
|
|
@ -12,7 +12,7 @@ test('chaque page publique s’affiche sans erreur', function () {
|
|||
&& $r->getName() !== null
|
||||
&& ! str_contains($r->uri(), '{')
|
||||
&& ! collect($r->gatherMiddleware())->contains(fn ($m) => is_string($m) && str_starts_with($m, 'auth')))
|
||||
->reject(fn ($r) => str_starts_with($r->uri(), 'horizon') || str_starts_with($r->uri(), '_') || $r->uri() === 'up')
|
||||
->reject(fn ($r) => str_starts_with($r->uri(), 'horizon') || str_starts_with($r->uri(), '_') || str_starts_with($r->uri(), 'interne/') || $r->uri() === 'up')
|
||||
// Redirections voulues (ex. /statut vers le site de statut ; pages de
|
||||
// suivi d'une demande pour rejoindre une classe, sans demande en cours).
|
||||
->reject(fn ($r) => $r->getActionName() === '\\Illuminate\\Routing\\RedirectController'
|
||||
|
|
@ -31,7 +31,8 @@ test('chaque page publique s’affiche sans erreur', function () {
|
|||
test('aucune page publique ne charge de police depuis un service externe', function () {
|
||||
$pages = collect(Route::getRoutes())
|
||||
->filter(fn ($r) => in_array('GET', $r->methods(), true) && $r->getName() !== null && ! str_contains($r->uri(), '{'))
|
||||
->reject(fn ($r) => collect($r->gatherMiddleware())->contains(fn ($m) => is_string($m) && str_starts_with($m, 'auth')));
|
||||
->reject(fn ($r) => collect($r->gatherMiddleware())->contains(fn ($m) => is_string($m) && str_starts_with($m, 'auth')))
|
||||
->reject(fn ($r) => str_starts_with($r->uri(), 'interne/'));
|
||||
|
||||
foreach ($pages as $route) {
|
||||
$html = $this->get('/'.ltrim($route->uri(), '/'))->getContent();
|
||||
|
|
@ -56,7 +57,8 @@ test('les scripts en ligne des pages publiques sont du JavaScript valide', funct
|
|||
$pages = collect(Route::getRoutes())
|
||||
->filter(fn ($r) => in_array('GET', $r->methods(), true) && $r->getName() !== null && ! str_contains($r->uri(), '{'))
|
||||
->reject(fn ($r) => collect($r->gatherMiddleware())->contains(fn ($m) => is_string($m) && str_starts_with($m, 'auth')))
|
||||
->reject(fn ($r) => $r->getActionName() === '\Illuminate\Routing\RedirectController');
|
||||
->reject(fn ($r) => $r->getActionName() === '\Illuminate\Routing\RedirectController')
|
||||
->reject(fn ($r) => str_starts_with($r->uri(), 'interne/'));
|
||||
|
||||
$erreurs = [];
|
||||
foreach ($pages as $route) {
|
||||
|
|
|
|||
69
portail/tests/Feature/WebdavTest.php
Normal file
69
portail/tests/Feature/WebdavTest.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\Eleve;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake('webdav');
|
||||
});
|
||||
|
||||
test('la sous-requête Nginx refuse sans identifiants', function () {
|
||||
$this->get(route('interne.webdav-auth'))->assertStatus(401);
|
||||
});
|
||||
|
||||
test('la sous-requête Nginx refuse un code secret incorrect', function () {
|
||||
$eleve = Eleve::factory()->create();
|
||||
|
||||
$this->withBasicAuth($eleve->identifiant, 'mauvais-code')
|
||||
->get(route('interne.webdav-auth'))
|
||||
->assertStatus(401);
|
||||
});
|
||||
|
||||
test('la sous-requête Nginx accepte les identifiants de connexion et pose le dossier de l’élève', function () {
|
||||
$eleve = Eleve::factory()->create(['code_secret' => bcrypt('monCode123')]);
|
||||
|
||||
$this->withBasicAuth($eleve->identifiant, 'monCode123')
|
||||
->get(route('interne.webdav-auth'))
|
||||
->assertStatus(200)
|
||||
->assertHeader('X-Eleve-Dossier', (string) $eleve->id);
|
||||
|
||||
Storage::disk('webdav')->assertExists((string) $eleve->id);
|
||||
});
|
||||
|
||||
test('5 échecs de suite bloquent temporairement même avec le bon code juste après', function () {
|
||||
$eleve = Eleve::factory()->create(['code_secret' => bcrypt('monCode123')]);
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$this->withBasicAuth($eleve->identifiant, 'mauvais')
|
||||
->get(route('interne.webdav-auth'))
|
||||
->assertStatus(401);
|
||||
}
|
||||
|
||||
$this->withBasicAuth($eleve->identifiant, 'monCode123')
|
||||
->get(route('interne.webdav-auth'))
|
||||
->assertStatus(401);
|
||||
|
||||
RateLimiter::clear('webdav:'.$eleve->identifiant.'|127.0.0.1');
|
||||
});
|
||||
|
||||
test('un élève connecté voit l’adresse de son dossier WebDAV', function () {
|
||||
$eleve = Eleve::factory()->create();
|
||||
|
||||
eleve($this, $eleve)->get(route('mes-fichiers'))
|
||||
->assertOk()
|
||||
->assertSee('/webdav/')
|
||||
->assertSee($eleve->identifiant);
|
||||
});
|
||||
|
||||
test('purger une classe archivée supprime aussi le dossier WebDAV de ses élèves', function () {
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
Storage::disk('webdav')->put($eleve->id.'/photo.jpg', 'contenu');
|
||||
|
||||
$classe->forceFill(['archivee_a' => now()->subDays(91)])->save();
|
||||
$this->artisan('classes:purger-archivees');
|
||||
|
||||
Storage::disk('webdav')->assertMissing($eleve->id.'/photo.jpg');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue