Repository navigation
Journal des connexions : traçabilité des accès et alerte sur contexte inconnu - #5
Merged
Merged
Conversation
… inconnu Le TDR exige des traces historisées systématiquement. Les tentatives d'accès n'étaient écrites que dans le journal applicatif (stdout de l'hébergeur) : ni requêtables, ni conservées. Impossible donc de répondre à « qui s'est connecté, depuis où, et quand » — ni de voir une série d'échecs. Table `login_events` (sans clé étrangère vers `users` : la trace doit survivre à la suppression du compte, et un échec sur une adresse inconnue n'a aucun utilisateur à référencer), alimentée aux quatre points de /auth : connexion réussie, échec avec son motif, déconnexion, prolongation de session. Plus `users.last_login_at` / `last_login_ip`. Une connexion est signalée « contexte inconnu » quand son adresse IP ou son appareil n'a jamais été observé pour ce compte ; la première connexion l'est donc toujours. Elle déclenche un courriel par le relais Brevo déjà en place, envoyé en tâche de fond pour ne pas peser sur le temps de connexion. Les identifiants SMTP n'existaient que sur les crons : ils sont ajoutés au service web, faute de quoi l'alerte serait restée inerte en production. Toute écriture passe par `record_safe` : un journal en panne dégrade la traçabilité, il ne doit pas fermer l'accès à la plateforme. Console de sécurité `/security/*` réservée au super-administrateur, en trois lectures : qui a tenté d'accéder, qui est connecté maintenant, et qui ne s'est jamais connecté. Ces endpoints passent par le rôle d'authentification et non par la session RLS : la politique `tenant_isolation_users` filtre par tenant sans échappatoire super-admin. Correction d'un défaut découvert au passage : `is_super_admin` n'était calculé nulle part, la revendication du jeton valait donc toujours faux. Toutes les portes qui en dépendent — bascule de tenant, contexte RLS élargi, écrans d'administration — étaient closes pour tout le monde. Elle est désormais lue depuis le rôle SUPER_ADMIN de `user_roles`, au login comme au renouvellement (sans quoi un refresh dégradait silencieusement le jeton). Aucun compte ne porte ce rôle par défaut : rien ne change pour les comptes existants. `app/scripts/grant_super_admin.py` permet de l'attribuer. 19 tests ajoutés (9 unitaires, 10 d'intégration). Migration s4 vérifiée à l'aller et au retour sur une base jetable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pourquoi
Le TDR exige des traces historisées systématiquement. Aujourd'hui, une tentative d'accès n'est écrite que dans le journal applicatif (stdout de l'hébergeur) : ni requêtable, ni conservée. On ne peut donc répondre ni à « qui s'est connecté, depuis où, et quand », ni voir une série d'échecs se former.
Ce que fait cette PR
Table
login_events, alimentée aux quatre points de/auth: connexion réussie, échec avec son motif (unknown_user/bad_password/disabled), déconnexion, prolongation de session. Plususers.last_login_at/last_login_ip.Deux choix de conception explicités dans le code : pas de clé étrangère vers
users— la trace doit survivre à la suppression du compte suspect, et un échec sur une adresse inconnue n'a par définition aucun utilisateur à référencer. Et toute écriture passe parrecord_safe: un journal en panne dégrade la traçabilité, il ne doit pas fermer l'accès à la plateforme.Détection de contexte inconnu — une connexion est signalée quand son adresse IP ou son appareil n'a jamais été observé pour ce compte. La première connexion d'un compte l'est donc toujours : c'est précisément l'événement qu'on attend après avoir remis des accès à un tiers.
Alerte par courriel via le relais Brevo déjà en place, envoyée en tâche de fond pour ne pas peser sur le temps de connexion. Les identifiants SMTP n'existaient que sur les services cron : ils sont ajoutés au service web dans
render.yaml, faute de quoi l'alerte serait restée inerte en production.Console de sécurité
/security/*, réservée au super-administrateur, en trois lectures complémentaires : qui a tenté d'accéder, qui est connecté maintenant (sessions ouvertes), et qui ne s'est jamais connecté (comptes dormants). Ces endpoints passent délibérément par le rôle d'authentification et non par la session RLS : la politique `tenant_isolation_users` filtre par tenant sans échappatoire super-admin, une session RLS ne verrait qu'un seul tenant.Correction d'un défaut découvert au passage
`is_super_admin` n'était calculé nulle part : la revendication émise dans l'access token valait toujours `false`. Toutes les portes qui en dépendent étaient donc closes pour tout le monde — bascule de tenant `X-Tenant-Id`, contexte RLS élargi, écrans d'administration.
Elle est désormais lue depuis le rôle `SUPER_ADMIN` de `user_roles`, au login comme au renouvellement (sans quoi un refresh dégradait silencieusement le jeton). Aucun compte ne porte ce rôle par défaut : rien ne change pour les comptes existants. L'attribution se fait par script :
```bash
python -m app.scripts.grant_super_admin conformite@bcrg-guinee.org
python -m app.scripts.grant_super_admin --list
```
Vérifications
s4_login_eventsappliquée et annulée sur une base jetable — colonnes, index etGRANTconformes ;ruff checkpropre.Après fusion
Le
preDeployCommandde Render appliquealembic upgrade headavant que le nouveau code ne prenne le trafic : la migration se pose seule. Restent à saisir dans le dashboard Render, sur le service web, les variablessync: false:BREVO_SMTP_LOGIN,BREVO_SMTP_KEY,BREVO_FROM_EMAIL,LOGIN_ALERT_TO_EMAIL. Sans elles, le journal enregistre tout normalement — seul l'envoi du courriel est ignoré.Le front correspondant (page « Journal de connexions ») fait l'objet d'une PR séparée sur le dépôt frontend. L'ordre de déploiement est indifférent.