Architecture
flowchart LR
N[Navigateur] --> P[PAFF, axum]
P <--> R[(Redis<br/>sessions et cache)]
P --> PASS[PASS / Alcuin]
P --> M[Moodle]
P --> I[Flux iCal]
P --> S[Rapports SSRS]
Authentification
CAS, puis IdP Shibboleth, puis PASS (src/auth/mod.rs). Les cookies obtenus vont dans
la session Redis.
Le pont ASP (init_asp_bridge) a son propre cookie jar. Fusionner ses cookies
avec ceux de la session, sinon PASS répond "Session perdue".
Moodle a son propre flux via le WAYF (src/modules/moodle.rs), une dizaine de
requêtes :
sequenceDiagram
participant PAFF
participant Moodle
participant WAYF
participant CAS
PAFF->>Moodle: GET launch.php
Note over PAFF,Moodle: pose tool_mobile_launch,<br/>obligatoire avant tout
PAFF->>Moodle: GET shibboleth/index.php
Moodle-->>WAYF: redirection
PAFF->>WAYF: POST, choix de l'IdP
WAYF-->>CAS: 4 redirections
PAFF->>CAS: POST des identifiants
PAFF->>CAS: POST du consentement Shibboleth
Note over PAFF,CAS: _eventId_proceed,<br/>jamais _eventId_AttributeReleaseRejected
PAFF->>Moodle: POST SAMLResponse à Shibboleth.sso
Moodle-->>PAFF: moodlemobile://token=BASE64
Note over PAFF: décodage SITE_ID:::TOKEN:::PRIVATE_TOKEN
La politique de redirection de reqwest s'arrête sur les schémas non HTTP : c'est ce qui permet d'attraper le token à l'étape 8.
Sources de données
| Donnée | Source | Piège |
|---|---|---|
| Horaires, salles | flux iCal | jointure sur l'UID PASS-{NumEve} |
| Type et couleur | HTML de Agenda.asp |
sans iCal, grille horaire, moins précise |
| Intervenants | Eve-Det.asp |
|
| Notes | export SSRS via ExportUrlBase |
HTML paginé : le premier <td> doit avoir la classe a145c |
| Cours | API REST Moodle | rapprochés des UE par recouvrement de mots |
| Annuaire | DataGrid de Contenu.aspx |
voir plus bas |
| Feuilles d'émargement | Moodle, cours 2579 | une fiche nominative par étudiant, rangée par campus |
L'appel DetEve(...) qui identifie un créneau est porté par onmouseover chez
la plupart des comptes et par onclick chez d'autres. C'est un réglage PASS propre à
chacun, que rien dans la page n'annonce. Lire tous les attributs on*.
Agenda.asp est piloté par le cookie ASP classique, pas par le jeton d'URL.
Sans ce cookie, PASS redirige vers /commun/erreur.asp : quelques centaines
d'octets sans aucun marqueur, qu'il ne faut pas lire comme une semaine vide.
L'adresse du flux iCal de chaque étudiant est distribuée par un webservice
(hub.imt-atlantique.fr/passtools/.../ws-getICSlocation.php) qui la rend à partir
du seul login, sans authentification, mais ne répond qu'au réseau de l'école :
403 à toute IP hors campus. Le serveur PAFF étant hébergé dehors, il ne peut pas
la lire. Le flux lui-même est public : une fois l'adresse connue, PAFF la relit
depuis n'importe où. C'est donc le navigateur de l'étudiant, sur eduroam ou VPN,
qui interroge le webservice (le script en bas de templates/planning.html, tant
qu'aucun flux n'est encore gardé) et repasse l'adresse à POST /api/ical/feed,
qui la relit avant de la garder. Le jeton étant stable, une seule capture au
premier passage sur le réseau école suffit pour toujours, et le flux resservira
ensuite depuis le VPS. La page Profil permet toujours de coller l'adresse à la
main.
GPA
Barème promo 2024 :
| Note | Intervalle | Coefficient |
|---|---|---|
| A | [17-20] | 4.0 |
| B | [14-17[ | 3.8 |
| C | [12-14[ | 3.5 |
| D | [10-12[ | 3.0 |
| E | rattrapage validé | 2.5 |
| Fx | [5-10[ | 0 |
| F | [0-5] | 0 |
GPA = somme(coeff x ECTS) / somme(ECTS). ECTS codés en dur dans
crates/paff-core/src/grades.rs.
Annuaire
- App ID 142 (190 = agenda).
- Charger tout le frameset avant que
Contenu.aspxréponde :
flowchart LR
A[Accueil.aspx] --> B[AnnTop.aspx] --> C[OngletsNavigation.aspx]
C --> D[NavigationBase.aspx] --> E[Recherche.aspx] --> F[Contenu.aspx]
- Changer d'annuaire : POST
Recherche.aspxavecddlAnnuaires=ID|30etbtnRecherche=Chercher. 134 = étudiants, 133 = personnels. - Pagination :
__doPostBack('objets$Objets$_dataGridPager', 'N'), quotes encodées'.NbLignesest ignoré par le serveur. - Photos publiques :
/DataOp/Outils/Annuaire/{GUID}.jpg, GUID dans lesrcde#imgUcImage1surDossier.aspx. - Les services SOPX renvoient "Access denied" à un étudiant, d'où le scraping.
- La recherche replie les accents : le planning écrit "BERANGERE" ce que l'annuaire écrit "Bérangère".
Émargement
La feuille est imprimée, signée en séance et déposée en papier. PAFF supprime la recopie, pas la signature.
Le texte d'une fiche PDF ne sort pas dans l'ordre du flux : l'extraction passe
par la position des glyphes (crates/paff-core/src/emargement/pdf.rs). Une
valeur longue finit collée à son libellé (cellules centrées) ; le libellé étant
un préfixe fixe, le reste de la ligne redonne la valeur. Une cellule trop
étroite imprime deux lignes avec le libellé centré entre elles.
Le code UE se lit sur le bloc imprimé en haut de la fiche
(emargement/ue_match.rs), qui renonce au premier doute. Le reste, l'étudiant
l'apparie à un code de son bloc (emargement/pairing.rs), une fois pour
toutes les semaines. Sa réponse compte aussi comme une voix par TAF : le code le
mieux soutenu l'emporte, une égalité ne donne rien, sa propre réponse prime
toujours. Le code est revérifié contre le bloc à chaque lecture et chaque
écriture.
Remplissage : réécriture d'une seule entrée du zip, en chaînes littérales. Les
durées sont formatées h:mm dans le classeur : écrire 3,5 afficherait une
date de janvier 1900. L'unité est la minute.
Chaîne de fabrication :
flowchart LR
T[Modèle xlsx<br/>assets/] --> F[Remplissage<br/>une entrée du zip]
F --> X[Classeur xlsx]
X --> L[LibreOffice<br/>service sheetengine]
L --> P[PDF sans bandeaux]
P --> H[letterhead.rs<br/>en-tête et pied réapposés]
H --> S[PDF final]
Les images d'en-tête et de pied de page sont les seules à ne pas survivre à la conversion. Comme elles sont opaques, la page est d'abord rétrécie des deux bandes qu'il leur faut.
Le modèle embarqué (assets/feuille-emargement.xlsx) est dérivé de la version
publiée par scripts/strip-emargement-template.py, qui retire aussi les chaînes
partagées orphelines, le nom de feuille, les métadonnées d'auteur et le chemin
réseau interne.
Les rattrapages d'une autre promo suivent la salle et atterrissent dans n'importe quel planning : retirés de la feuille, gardés dans le planning.
Signature manuscrite
La capture enregistre le geste, position et instant de chaque point, parce que les modèles cinématiques qui le rejouent travaillent sur le profil de vitesse. C'est la donnée la plus sensible du projet : elle n'est écrite dans Redis que chiffrée.
O'Reilly & Plamondon, Development of a Sigma-Lognormal representation for on-line signatures, Pattern Recognition 42(12), 2009. https://doi.org/10.1016/j.patcog.2008.10.017
XChaCha20-Poly1305 (crates/paff-core/src/signature/vault.rs), nonce de 192
bits tiré à chaque écriture, identifiant du propriétaire lié en données
associées, si bien que déplacer une enveloppe vers un autre compte fait échouer
le déchiffrement. Clé dans PAFF_SIGNATURE_KEY. L'enveloppe porte l'empreinte
tronquée de la clé qui l'a scellée : ouvrir est une recherche dans le trousseau,
pas une suite d'essais.
Ce que ça protège : ce que Redis écrit à partir du scellement (AOF, RDB,
sauvegardes), plus une session redis-cli et une image disque. Ce que Redis a
écrit avant reste en clair jusqu'au prochain BGREWRITEAOF, à lancer après
le premier scellement, et à vérifier. Qui peut lire l'environnement du processus
a la clé. Ce n'est pas du bout en bout.
Sans clé, une instance qui a ouvert la fonction refuse d'enregistrer (503) et le dit au démarrage.
Rotation : PAFF_SIGNATURE_KEY scelle, PAFF_SIGNATURE_KEY_PREVIOUS (virgules)
ouvre encore. Le rescellement se fait à la lecture, sans migration ni
re-signature. Retirer la clé sortante trop tôt ne perd rien : les enveloppes sont
refusées, pas écrasées (SignatureLibrary::unreadable). Procédure complète dans
.env.example.
Contraintes de code
scraper::Htmletscraper::Selectorutilisent desRc: jamais à travers un.await. Tout le parsing HTML est synchrone.paff-corene dépend ni de reqwest, ni de tokio, ni d'axum.
Cache Redis
| Clé | Contenu | TTL |
|---|---|---|
session:{uuid} |
session utilisateur | SESSION_TTL |
events:{group}:{user}:{semaine} |
une semaine | EVENT_CACHE_TTL |
events:year:{group}:{user} |
l'année, pour /stats et l'émargement | 24 h |
detail:{event_id}:{date} |
prof et salle | DETAIL_CACHE_TTL |
grades:report:{user_id} |
relevé analysé | GRADES_CACHE_TTL |
grades:seen:{user_id} |
UE déjà notées | 400 j |
ects:{code} |
crédits lus chez FPC | 30 j |
annuaire:{dir} |
annuaire complet, partagé | 6 h |
emarg:header:{user_id} |
en-tête de fiche | EMARGEMENT_HEADER_TTL |
emarg:ue:{campus}:{taf} |
liste d'UE, partagée par TAF | EMARGEMENT_UE_TTL |
emarg:files:{course_id} |
listing du cours Moodle | 1 h |
emarg:ue-pairing:{user_id} |
appariements manuels | 365 j |
emarg:ue-shared:{campus}:{taf} |
une réponse par personne et par intitulé | 365 j |
ical_token:{token} / ical_user:{id} |
abonnement au flux exporté | 7 j, prolongés à chaque lecture |
signature:trace:{user_id} |
tracés chiffrés | dernière échéance des gestes |
Les événements sont indexés par utilisateur, pas par groupe : le NomCal
(USR{user_id}) filtre différemment pour deux étudiants d'un même groupe.
Priorité à la page affichée
PASS traite les requêtes d'un utilisateur l'une après l'autre. Les travaux de
fond (188 pages d'annuaire, l'année scannée, le catalogue FPC) empruntent sa
session et passeraient devant la page qu'il demande. src/pass_priority.rs
compte les requêtes attendues ; chaque travail de fond s'efface tant que ce
compte n'est pas nul.
Le grain diffère : l'index de fiches et le catalogue cèdent avant chaque requête, l'annuaire par lots de six pages, le rattrapage des enseignants par quarante. Un lot commencé va au bout.
Les marqueurs "une tâche s'en occupe" (scan:lock:*, annuaire_lock:* et les
autres) sont
libérés au démarrage : un redémarrage tue les tâches, pas les marqueurs, et le
travail restait refusé jusqu'à leur expiration.