166 lines
9.1 KiB
Markdown
166 lines
9.1 KiB
Markdown
# PTA V7.11 — Connexion par jeton, espace DSI et journal d'activité
|
|
|
|
## Le problème traité
|
|
|
|
Depuis l'origine, `role.php` était un **sélecteur, pas une authentification** : n'importe qui
|
|
ouvrant l'application choisissait librement son rôle et son identité, y compris « Service Enfance »,
|
|
qui valide les plannings de toute la collectivité. Les READMEs successifs et `CLAUDE.md` la
|
|
désignaient comme *la faille ouverte la plus importante du projet*.
|
|
|
|
Deux conséquences, au-delà du contrôle d'accès lui-même :
|
|
|
|
- **aucune action n'était imputable** — un planning validé, un agent modifié, un lieu créé ne
|
|
portaient le nom de personne ;
|
|
- **rien n'était traçable** : pas de journal, donc aucune façon de répondre à « qui a rouvert cette
|
|
semaine ? » ou « depuis quand cet accès est-il utilisé ? ».
|
|
|
|
## Ce qui change
|
|
|
|
### 1. On entre avec un jeton, plus avec un choix
|
|
|
|
Chaque utilisateur — agent, responsable de lieu, administrateur du Service Enfance, DSI — reçoit un
|
|
**jeton personnel** de la forme `PTA-XXXXXX-XXXXXX-XXXXXX-XXXXXX`, tiré au sort par le serveur sur
|
|
120 bits d'entropie, dans un alphabet sans caractères ambigus (ni `I`, ni `O`, ni `0`, ni `1`).
|
|
|
|
Il n'y a **aucun mot de passe** dans PTA : rien à choisir, rien à renouveler périodiquement, aucune
|
|
procédure de réinitialisation à écrire. Un jeton perdu ne se retrouve pas, il se **régénère**, ce qui
|
|
révoque l'ancien à la seconde.
|
|
|
|
Deux façons de le présenter, l'une et l'autre acceptées :
|
|
|
|
- le **champ de saisie** de `role.php` (POST) — le jeton ne transite alors ni par l'URL, ni par
|
|
l'historique du navigateur, ni par les journaux de nginx ;
|
|
- le **lien direct** `role.php?token=…` — plus commode à envoyer par courriel, mais le jeton
|
|
s'inscrit dans l'historique et les journaux du serveur web, et un lien transféré donne l'accès.
|
|
La session ouverte, une redirection immédiate retire le jeton de la barre d'adresse.
|
|
|
|
Le rôle, l'agent et le lieu ne sont plus déclarés par l'utilisateur : ils sont **lus sur son compte**.
|
|
Le reste de la mécanique de droits n'a pas bougé d'une ligne — `Access` interroge les mêmes vues SQL
|
|
qu'en V7.8.
|
|
|
|
### 2. La base ne conserve jamais le jeton
|
|
|
|
`compte_acces` ne stocke qu'une **empreinte SHA-256** du jeton et ses onze premiers caractères, qui
|
|
servent uniquement à reconnaître une ligne dans la liste. Un vol du dump SQL ou un accès à phpMyAdmin
|
|
ne donne donc **aucun accès applicatif**.
|
|
|
|
Conséquence assumée, visible partout dans l'interface : **un jeton n'est affiché qu'une seule fois**,
|
|
dans le panneau « Jetons à transmettre » qui suit sa création. Il faut le copier à ce moment-là.
|
|
|
|
L'empreinte n'est volontairement pas salée, contrairement à ce qu'on ferait d'un mot de passe : un
|
|
jeton est tiré par le serveur, pas devinable par dictionnaire, et l'absence de sel est ce qui permet
|
|
de retrouver le compte en **une lecture d'index** à la connexion plutôt qu'en comparant à chaque
|
|
compte.
|
|
|
|
### 3. Un quatrième rôle : DSI
|
|
|
|
`DSI` rejoint `AGENT`, `RESPONSABLE_STRUCTURE` et `SERVICE_ENFANCE` dans `acces_role_page` et
|
|
`acces_role_permission`. C'est une administration **technique**, pas fonctionnelle : la DSI ne voit
|
|
ni planning, ni quota, ni agent — une seule page lui est ouverte, `dsi.php`, qui contient :
|
|
|
|
- **les comptes d'accès** : création, régénération de jeton, désactivation, suppression, filtres par
|
|
type, par état et par nom ;
|
|
- **« Générer les jetons manquants »** : équipe d'un coup tous les agents et tous les lieux actifs
|
|
qui n'ont pas encore de compte. C'est l'outil de mise en service — sans lui, personne ne pourrait
|
|
entrer le jour du déploiement ;
|
|
- **le journal d'activité** : recherche filtrée et paginée, quatre chiffres de tête, purge des
|
|
entrées anciennes.
|
|
|
|
Deux permissions nouvelles, `can_manage_access` et `can_read_journal`, sont accordées à la seule DSI
|
|
et **refusées explicitement** aux trois rôles métier.
|
|
|
|
### 4. Ce qui est journalisé — et ce qui ne l'est pas
|
|
|
|
`journal_activite` est alimenté par `App\Core\Journal`, jamais par des déclencheurs SQL : la base ne
|
|
sait pas qui agit, la connexion MariaDB étant la même pour tout le monde.
|
|
|
|
| Catégorie | Contenu |
|
|
| --- | --- |
|
|
| `CONNEXION` | Connexion réussie, jeton refusé, blocage anti-force-brute, déconnexion |
|
|
| `ECRITURE` | Toute modification en base, avec la table et l'identifiant visés |
|
|
| `ACCES_REFUSE` | Page interdite, rôle insuffisant, périmètre dépassé, jeton CSRF invalide |
|
|
| `ADMINISTRATION` | Actions de la DSI sur les comptes et le journal, purge comprise |
|
|
| `SYSTEME` | Réservé aux incidents techniques |
|
|
|
|
**Les lectures ne sont pas tracées** : elles représenteraient l'essentiel du volume pour une valeur
|
|
d'audit très faible.
|
|
|
|
Deux garanties tenues par le code :
|
|
|
|
- écrire dans le journal **ne peut jamais faire échouer l'action journalisée** — toute erreur
|
|
d'insertion est avalée et renvoyée dans `error_log` ;
|
|
- la trace est **figée** : rôle et libellé de l'acteur sont copiés au moment de l'action, et la clé
|
|
étrangère vers `compte_acces` est en `ON DELETE SET NULL`. Supprimer un compte n'efface pas son
|
|
histoire, et renommer un agent ne la réécrit pas.
|
|
|
|
### 5. Freins et garde-fous
|
|
|
|
- **Anti-force-brute** : au-delà de 10 jetons refusés en 15 minutes depuis une même adresse IP, plus
|
|
aucun jeton n'est même comparé, et le blocage est journalisé. Le compteur, c'est le journal
|
|
lui-même — aucune table supplémentaire.
|
|
- **Révocation immédiate** : `Access` relit le compte à **chaque requête**. Désactiver un compte,
|
|
désactiver l'agent qui le porte ou fermer son lieu referme la session en cours à la requête
|
|
suivante, sans attendre son expiration.
|
|
- **On ne se coupe pas la branche** : la DSI ne peut ni désactiver ni supprimer le compte avec lequel
|
|
elle est connectée, et le modèle refuse de retirer le dernier accès DSI actif.
|
|
- **Purge plancher à 30 jours** : une purge plus courte reviendrait à effacer les traces d'un
|
|
incident avant même qu'il soit constaté.
|
|
|
|
## Mise en service
|
|
|
|
1. Jouer les migrations (`docker compose up -d --build` s'en charge) ; `migration_v7_9e_…` crée les
|
|
tables, les vues, le rôle DSI et **un compte DSI d'installation**.
|
|
2. Se connecter avec le **jeton d'installation**, écrit en clair dans la migration :
|
|
|
|
```
|
|
PTA-INSTALLATION-DSI
|
|
```
|
|
|
|
Il est **public** — il est versionné dans le dépôt. La page DSI affiche un bandeau rouge tant
|
|
qu'il n'a pas été remplacé.
|
|
3. **Régénérer immédiatement ce jeton** (bouton « Régénérer » sur la ligne du compte DSI), le copier
|
|
et le conserver. Créer au passage un second compte DSI si plusieurs personnes doivent administrer.
|
|
4. Cliquer sur **« Générer les jetons manquants »** : un jeton par agent et par lieu actif. Le
|
|
panneau les affiche tous, une seule fois — les copier avant de le refermer, puis les distribuer.
|
|
5. Créer les comptes **Service Enfance** un par un (ils ne correspondent à aucune identité en base,
|
|
leur libellé est saisi à la main).
|
|
|
|
### Perte du jeton DSI
|
|
|
|
C'est le seul cas qui ne se résout pas dans l'interface. Procédure de secours, en SQL :
|
|
|
|
```sql
|
|
DELETE FROM compte_acces WHERE id_compte = 1;
|
|
```
|
|
|
|
puis rejouer la migration (`docker compose run --rm db-migrate` après avoir touché le fichier, ou
|
|
directement l'`INSERT` de la section 3 de la migration) : le compte d'installation est recréé avec
|
|
son jeton public, à régénérer aussitôt.
|
|
|
|
## Fichiers
|
|
|
|
| Fichier | Rôle |
|
|
| --- | --- |
|
|
| `database/migration_v7_9e_dsi_comptes_journal.sql` | Rôle DSI, `compte_acces`, `journal_activite`, trois vues, compte d'installation |
|
|
| `app/Core/Journal.php` | Écriture des traces ; connexion posée par `app/bootstrap.php` |
|
|
| `app/Core/Access.php` | `ROLE_DSI`, `openSession()` (remplace `select()`), relecture du compte, journalisation des refus |
|
|
| `app/Models/CompteModel.php` | Jetons : génération, empreinte, authentification, cycle de vie des comptes |
|
|
| `app/Models/JournalModel.php` | Lecture paginée, statistiques, compteur d'échecs, purge |
|
|
| `app/Controllers/RoleController.php` | Connexion par jeton (champ et lien), déconnexion, frein anti-force-brute |
|
|
| `app/Controllers/DsiController.php` | Les sept endpoints de l'espace DSI |
|
|
| `dsi.php`, `api/dsi_*.php` | Points d'entrée minces |
|
|
| `app/Views/pages/role.php` | Écran de connexion (le sélecteur de rôle a disparu) |
|
|
| `app/Views/partials/dsi.php`, `assets/js/dsi.js` | Interface de l'espace DSI |
|
|
|
|
## Ce que la V7.11 ne fait pas
|
|
|
|
- **Pas d'annuaire ni de SSO** : le jeton reste un secret partagé hors de l'application (courriel,
|
|
papier). Un raccordement LDAP ou OpenID Connect resterait à faire, et ne toucherait que
|
|
`RoleController` et `CompteModel`.
|
|
- **Pas d'expiration automatique des jetons** : un jeton vit tant que la DSI ne le régénère pas. Une
|
|
date de péremption serait une colonne de plus sur `compte_acces` et une condition de plus dans
|
|
`v_acces_compte`.
|
|
- **Pas d'export du journal** : il se consulte à l'écran, filtre par filtre. Un export CSV
|
|
demanderait un endpoint supplémentaire.
|
|
- **Pas de purge automatique** : la purge est déclenchée à la main par la DSI, et journalisée.
|