Aller au contenu
English

Auditabilité

  • Leçon 18
  • 45 min
  • Module 6 : Valider et documenter la conception

Ce que vous saurez faire

Faites consigner chaque décision d’accès par l’API : qui a agi, dans quelle organisation, ce qu’il a tenté et le résultat. Consignez aussi les refus, sans jeton ni secret dans l’enregistrement. Vérifiez ensuite que l’enregistrement répond à deux questions : qui a fait quoi et qui a essayé.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le projet après Tests négatifs, avec tests/test_access_matrix.py.
  • PostgreSQL et Azurite en marche, les données chargées et deux autres terminaux : le fournisseur (python -m idp) et l’API (fastapi dev).

Chaque requête refusée dans cette leçon est un cas construit, envoyé à votre propre API d’exercice. Testé sur macOS (Apple silicon); les commandes Windows sont fournies, mais n’ont pas été testées.

Un immeuble de bureaux a un comptoir d’accueil et un registre des visiteurs. Un bon registre contient une ligne par visite : le nom, l’entreprise visitée, l’heure et la personne qui a autorisé l’entrée. Imaginez maintenant que le gardien note seulement les visiteurs qui sont entrés. Un inconnu essaie la porte cinq fois la nuit et se fait refuser l’entrée cinq fois. Le registre ne montre rien. La semaine suivante, le propriétaire demande : « Quelqu’un a-t-il essayé d’entrer? » Le registre ne peut pas répondre.

Le registre ne doit pas non plus contenir les mauvaises informations. Si le gardien y copie le code d’accès de chaque visiteur, le registre devient lui-même une clé.

Opérations privilégiées a ajouté la table audit_events. Elle consigne déjà les changements, comme une invitation, et chaque utilisation de l’accès de soutien. Consigne-t-elle les refus? Essayez. Chargez les données à nouveau pour que la table soit vide au départ :

Fenêtre de terminal
python -m scripts.load --reset

Envoyez ensuite quatre requêtes : trois qui doivent échouer et une qui doit réussir.

  1. Obtenez les jetons de trois personnes. Sur Windows, tapez curl.exe au lieu de curl dans chaque commande ci-dessous :

    Fenêtre de terminal
    OMAR=$(python -m scripts.login --user usr-omar --print-access-token)
    SAM=$(python -m scripts.login --user usr-sam --print-access-token)
    GRACE=$(python -m scripts.login --user usr-grace --print-access-token)
  2. Omar, en lecture seule, essaie d’ajouter un message. Sam tente d’accéder au billet de soutien de Bramble. Quelqu’un envoie un jeton invalide :

    Fenêtre de terminal
    curl -X POST -H "Authorization: Bearer $OMAR" -H "Content-Type: application/json" -d '{"author": "agent", "body": "Refund approved."}' http://127.0.0.1:8000/v1/tenants/larkfield/tickets/T-30002/messages
    curl -H "Authorization: Bearer $SAM" http://127.0.0.1:8000/v1/tenants/bramble/tickets/T-40001
    curl -H "Authorization: Bearer abc.def" http://127.0.0.1:8000/v1/tenants/larkfield/tickets
    {"error":{"code":"forbidden","message":"Your role may not do this (read_only may not ticket.write).","request_id":"98e78665f77d","fields":[]}}
    {"error":{"code":"not_found","message":"Not found.","request_id":"e0fd09350d86","fields":[]}}
    {"error":{"code":"invalid_token","message":"The token is not a JWT.","request_id":"1c5c42146725","fields":[]}}

    L’API refuse les trois requêtes : 403, 404 et 401. Vos identifiants de requête diffèrent.

    Dans PowerShell, placez le corps JSON dans un fichier, msg.json, et utilisez -d "@msg.json".

  3. Grace invite une nouvelle personne :

    Fenêtre de terminal
    curl -X POST -H "Authorization: Bearer $GRACE" -H "Content-Type: application/json" -d '{"email": "dana@larkfield.example", "role": "staff"}' http://127.0.0.1:8000/v1/tenants/larkfield/invitations
  4. Regardez la table d’audit dans psql :

    SELECT event_id, actor_id, tenant_id, action, result, reason FROM audit_events ORDER BY event_id;
    event_id | actor_id | tenant_id | action | result | reason
    ----------+-----------+-----------+--------------------+--------+--------
    1 | usr-grace | larkfield | invitation.created | done |
    (1 row)

La table sait que Grace a invité quelqu’un. Elle ne sait rien d’Omar, de Sam ou du jeton invalide. L’API a correctement refusé les trois requêtes, puis les a oubliées. Si Omar tente d’accéder à chaque billet, chaque nuit, personne ne peut le voir.

L’idée : un événement d’audit pour chaque décision

Section intitulée « L’idée : un événement d’audit pour chaque décision »

Un événement d’audit consigne une décision : qui a agi (l’acteur), dans quelle organisation, quelle action, sur quoi (la cible) et le résultat. Une piste d’audit est la liste de ces événements. Elle permet de répondre plus tard à deux questions : « Qui a fait quoi? » et « Qui a essayé quoi? » La deuxième question exige de consigner les refus.

Trois règles rendent une piste utile :

  1. Chaque décision, y compris les refus : un jeton refusé, une personne sans adhésion, un rôle insuffisant.
  2. Jamais de secret. Aucun jeton, témoin, secret client, code d’invitation, mot de passe ou texte de message. La piste doit pouvoir être montrée sans risque à un propriétaire ou à un auditeur.
  3. Ajout seulement. Personne ne modifie ou ne supprime un événement, pas même l’API. Une piste modifiable ne prouve rien.

Une autre question détermine où placer un événement. Sam a tenté d’accéder au billet de Bramble. Ines, propriétaire de Bramble, devrait-elle voir la tentative de Sam dans l’audit de sa boutique? Cela lui révélerait qu’une personne de Larkfield appelée usr-sam existe. Le 404 reçu par la personne externe cache l’existence de l’enregistrement; la piste ne doit pas non plus la révéler. On consigne donc la tentative d’une personne externe sans organisation : l’équipe de la plateforme la voit, mais aucune boutique. Le refus d’un membre (403) appartient à cette organisation : le membre la connaît déjà.

Une requête passe par quatre vérifications. 1. Jeton ou session validé? Non : 401, événement token.check refusé, aucun acteur, aucun locataire, la raison est la vérification, jamais le jeton. 2. Membre de cette organisation? Non : 404, événement refusé avec la raison not_a_member, la personne comme acteur, locataire NULL, le chemin dans les détails. 3. Le rôle et la portée permettent l'action? Non : 403, événement refusé, raison role_cannot, la personne et l'organisation. 4. L'enregistrement est dans cette organisation? Non : 404 n'existe pas, aucun événement, seulement la ligne du journal de l'API. Oui : le changement et son événement dans une seule transaction, par exemple invitation.created.
Chaque vérification qui refuse écrit un événement, et chaque événement indique seulement ce que cette vérification savait. Le 404 de la recherche n'écrit aucun événement.
Description textuelle du diagramme

Une requête passe par quatre vérifications dans l’ordre. Si le jeton ou la session échoue, l’API répond 401 et consigne token.check comme refusé. La raison est le nom de la vérification; il n’y a ni acteur, ni organisation, et jamais de jeton. Si la personne n’est pas membre, l’API répond 404 et consigne la personne, sans organisation, avec le chemin dans les détails. Si le rôle ou la portée est insuffisant, l’API répond 403 et consigne la personne et l’organisation. Si l’enregistrement n’est pas dans l’organisation, l’API répond 404 « n’existe pas » et écrit seulement une ligne de journal, sans événement. Si tout réussit, l’API écrit le changement et son événement dans une seule transaction.

Regardez la vérification 4. Un membre demande l’identifiant d’un billet d’une autre boutique par son propre chemin. Il passe les trois premières vérifications, et la recherche ne trouve rien. L’API ne peut pas distinguer un identifiant deviné d’une faute de frappe. Ce cours ne consigne donc pas ce cas comme une tentative. Ce choix a un coût, que vous voyez dans les résultats ci-dessous.

Le changement est petit : trois endroits dans l’API appellent audit.record() lorsqu’ils refusent. Dans ticket_api/tenancy.py, la vérification de chaque route protégée fait maintenant ceci :

ticket_api/tenancy.py (part)
if not decision.allowed:
# Module 6: who TRIED is as important as who did. The tenant is recorded only for a
# member (403): an outsider's attempt is not written into another organization's log.
if grant is None:
audit.record(
db,
action=action,
result="denied",
actor_id=identity.user_id,
actor_kind=identity.kind,
tenant_id=tenant_id if decision.status == 403 else None,
target=f"{request.method} {request.url.path}",
reason=decision.code,
request_id=getattr(request.state, "request_id", None),
details={"rule": decision.rule, "tenant_in_path": tenant_id},
)
raise AccessDenied(decision, tenant_id, action)

grant is None exclut une autorisation de soutien. L’accès de soutien consigne déjà chaque utilisation, permise ou refusée, depuis Opérations privilégiées. audit.record() écrit l’événement dans sa propre transaction courte, car une requête refusée ne valide rien d’autre. Si la base de données ne peut pas écrire l’événement, l’API répond quand même et consigne l’échec dans le journal.

Dans ticket_api/errors.py, un jeton refusé devient l’événement token.check, avec le nom de la vérification comme raison (expired (expiré), wrong_audience (mauvaise audience)…), sans jeton. Dans ticket_api/login.py, une connexion, une déconnexion et une connexion avec un état inconnu ont aussi leurs événements.

  1. Copiez les fichiers de cette leçon depuis l’instantané end-of-m06 : les trois modules modifiés, le script d’audit et son test.

    Fenêtre de terminal
    S=../nextia-learning-labs/C23/snapshots/end-of-m06
    for f in ticket_api/tenancy.py ticket_api/errors.py ticket_api/login.py \
    scripts/audit.py tests/test_auditability.py; do
    cp "$S/$f" "$f"
    done

    fastapi dev détecte les fichiers modifiés et redémarre l’API.

  2. Chargez les données à nouveau et envoyez les mêmes quatre requêtes (étapes 2 et 3 de la première liste). Les réponses ne changent pas : 403, 404, 401 et 201.

  3. Lisez la piste avec le nouveau script :

    Fenêtre de terminal
    python -m scripts.audit
    07:12:20 usr-omar larkfield ticket.write denied role_cannot POST /v1/tenants/larkfield/tickets/T-30002/messages
    07:12:20 usr-sam - ticket.read denied not_a_member GET /v1/tenants/bramble/tickets/T-40001
    07:12:20 (no identity) - token.check denied malformed GET /v1/tenants/larkfield/tickets
    07:12:20 usr-grace larkfield invitation.created done dana@larkfield.example

    Chaque ligne contient l’heure (UTC), l’acteur, l’organisation (- pour aucune), l’action, le résultat, la raison et la cible. La table conserve aussi l’identifiant de requête que l’API renvoie dans chaque corps d’erreur. Quand Omar signale « J’ai eu une erreur », l’équipe de soutien peut retrouver son événement exact.

Lisez les quatre lignes avec les trois questions de Kwame. Omar : on sait qui il est, l’organisation est Larkfield, l’action ticket.write a été refusée parce que son rôle ne la permet pas. Sam : on sait qui il est, mais aucune organisation n’est indiquée; sa tentative n’est pas dans la piste de Bramble. Le jeton invalide : aucun acteur, car un jeton qui échoue à sa vérification ne prouve l’identité de personne. Grace : un changement, effectué.

Filtrez la piste par organisation et résultat, comme le ferait le propriétaire de Larkfield :

Fenêtre de terminal
python -m scripts.audit --tenant larkfield --result denied
07:12:20 usr-omar larkfield ticket.write denied role_cannot POST /v1/tenants/larkfield/tickets/T-30002/messages

Les dix tentatives inventées de L’autorisation dans l’API échouent encore et laissent maintenant une piste :

Fenêtre de terminal
python -m scripts.load --reset >/dev/null
python -m scripts.attempts 2>/dev/null | tail -1
python -m scripts.audit --result denied
0 of 10 attempts got what they should not get.
07:12:22 usr-sam - ticket.read denied not_a_member GET /v1/tenants/bramble/tickets/T-40001
07:12:22 usr-omar larkfield ticket.write denied role_cannot POST /v1/tenants/larkfield/tickets/T-30002/messages
07:12:22 usr-tomas - ticket.read denied not_a_member GET /v1/tenants/larkfield/tickets/T-30002
07:12:22 usr-camille bramble ticket.write denied role_cannot POST /v1/tenants/bramble/tickets/T-40001/messages

Dans PowerShell, écrivez | Out-Null au lieu de >/dev/null, 2>$null au lieu de 2>/dev/null et Select-Object -Last 1 au lieu de tail -1. Dix tentatives, quatre événements. Comptez les tentatives manquantes :

Tentative Réponse Dans la piste? Pourquoi
1. Sam, identifiant de Bramble par le chemin de Larkfield 404 « n’existe pas » non vérification 4 : la recherche n’a rien trouvé
2. Sam, par le chemin de Bramble 404 oui, sans organisation pas membre
3, 6. Ines consulte la liste et fait une recherche dans sa propre boutique 200 non les lectures permises ne sont pas consignées
4. Omar ajoute un message 403 oui, Larkfield rôle insuffisant
5. Tomás lit T-30002 404 oui, sans organisation pas membre
7. En-tête X-Tenant-ID falsifié de Sam 200, Larkfield seulement non l’API ignore l’en-tête
8. Camille écrit chez Bramble Books 403 oui, Bramble rôle insuffisant
9. Sam, identifiant d’une pièce jointe de Bramble 404 « n’existe pas » non vérification 4
10. Recherche d’Omar pour des documents du personnel 200, documents publics seulement non une lecture permise

La piste montre quatre tentatives. Les tentatives 1 et 9 utilisent des identifiants devinés de l’autre boutique. C’est l’attaque au niveau des objets la plus typique, et la piste ne peut pas les voir. Elles figurent seulement dans le journal de l’API, comme lignes 404. Pour détecter les identifiants devinés, comptez les réponses 404 d’une personne dans le journal au fil du temps. Déclenchez une alerte si elles sont nombreuses. Ce cours ne crée pas cette alerte.

Trois groupes d'événements. Audit de Larkfield, lu par Grace, la propriétaire : usr-omar, ticket.write, refusé, role_cannot; usr-grace, invitation.created, effectué. Audit de Bramble Books, lu par Ines, la propriétaire : usr-camille, ticket.write, refusé, role_cannot. Sans organisation, visible seulement par l'équipe de la plateforme : usr-sam, ticket.read, refusé, not_a_member, chemin bramble; usr-tomas, ticket.read, refusé, not_a_member, chemin larkfield; aucune identité, token.check, refusé, malformed.
À qui appartient la piste? Le refus d'un membre va à l'organisation; la tentative d'une personne externe et un jeton refusé vont seulement à l'équipe de la plateforme.
Description textuelle du diagramme

Les événements de cette leçon se répartissent en trois groupes. Grace, propriétaire de Larkfield, voit l’écriture refusée d’Omar et sa propre invitation. Ines, propriétaire de Bramble, voit l’écriture refusée de Camille. Certains événements n’ont pas d’organisation : les tentatives de Sam et de Tomás comme personnes externes, et le jeton invalide. Seule l’équipe de la plateforme les voit, avec le chemin dans les détails.

Vérifiez par l’API, comme les propriétaires le voient :

Fenêtre de terminal
curl -H "Authorization: Bearer $GRACE" http://127.0.0.1:8000/v1/tenants/larkfield/audit
{"items":[{"event_id":2,"at":"2026-10-10T07:12:22.220942Z","actor_id":"usr-omar","actor_kind":"user",
"tenant_id":"larkfield","action":"ticket.write","target":"POST /v1/tenants/larkfield/tickets/T-30002/messages",
"result":"denied","reason":"role_cannot","request_id":"9e9a040b5bfa",
"details":{"rule":"read_only may not ticket.write","tenant_in_path":"larkfield"}}]}

La réponse est divisée en lignes ici. Grace voit un événement : celui d’Omar. La tentative de Tomás chez Larkfield n’est pas dans sa piste, car il n’est pas membre. Maintenant, Ines lit la piste de Bramble. Obtenez d’abord son jeton, comme à l’étape 1 :

Fenêtre de terminal
INES=$(python -m scripts.login --user usr-ines --print-access-token)
curl -H "Authorization: Bearer $INES" http://127.0.0.1:8000/v1/tenants/bramble/audit

Dans PowerShell : $INES = python -m scripts.login --user usr-ines --print-access-token, puis curl.exe comme avant. Sa réponse contient un événement, l’écriture refusée de Camille (event_id (identifiant d’événement) 4, reason (raison) role_cannot (le rôle ne le permet pas)), sans ligne pour Sam.

Faites une prédiction avant d’exécuter la commande : Sam, employé chez Larkfield, demande la piste de Larkfield. Quel statut et quel événement?

Fenêtre de terminal
curl -H "Authorization: Bearer $SAM" http://127.0.0.1:8000/v1/tenants/larkfield/audit
python -m scripts.audit --actor usr-sam
{"error":{"code":"forbidden","message":"Your role may not do this (staff may not audit.read).","request_id":"6a48c96f8169","fields":[]}}
07:12:22 usr-sam - ticket.read denied not_a_member GET /v1/tenants/bramble/tickets/T-40001
07:12:22 usr-sam larkfield audit.read denied role_cannot GET /v1/tenants/larkfield/audit

Seul un propriétaire peut lire la piste, donc Sam reçoit 403. Son nouvel événement indique Larkfield comme organisation, car il y est membre. Sa tentative précédente chez Bramble Books n’en indique aucune.

Kwame vérifie la deuxième règle : aucun secret dans la piste. scripts.audit --check lit chaque événement et cherche un JWT, le mot Bearer (porteur) et chaque secret de votre .env :

Fenêtre de terminal
python -m scripts.audit --check
Checked 5 audit events: 0 with a token or a secret.

Voici maintenant une erreur construite. Imaginez qu’un développeur consigne l’en-tête Authorization (autorisation) de la requête « pour le débogage ». Insérez un tel événement à la main dans psql, avec un jeton inventé :

INSERT INTO audit_events (actor_id, actor_kind, action, result, details)
VALUES ('usr-sam', 'user', 'debug.request', 'done',
'{"header": "Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3Itc2FtIn0.c2lnbmF0dXJl"}');
Fenêtre de terminal
python -m scripts.audit --check
event 6 (debug.request) has a JWT, Bearer
Checked 6 audit events: 1 with a token or a secret.

Le script se termine avec le code 1, ce qui permet à une tâche planifiée ou une étape CI d’échouer. Essayez maintenant de supprimer le mauvais événement :

DELETE FROM audit_events WHERE event_id = 6;
ERROR: audit events cannot be changed or deleted
CONTEXT: PL/pgSQL function audit_events_append_only() line 3 at RAISE

Votre identifiant d’événement peut différer. C’est la troisième règle en action, et elle a deux effets. Personne ne peut cacher un événement, mais personne ne peut facilement supprimer un secret divulgué non plus. Un jeton dans une piste à ajout seulement y reste. C’est pourquoi audit.write(), qu’appelle aussi audit.record(), nettoie details (détails) avant d’écrire. Les clés qui nomment un secret sont supprimées, et toute valeur ressemblant à un JWT devient [removed] (supprimé). L’événement construit a contourné ce code pour aller directement dans la table. Dans votre base de données d’exercice, python -m scripts.load --reset vide la table. TRUNCATE n’active pas le déclencheur, donc un propriétaire de la base de données peut encore tout effacer. Dans un vrai système, il faut révoquer immédiatement un jeton divulgué, car il reste dans la piste.

  1. Nettoyez : python -m scripts.load --reset, puis python -m scripts.audit --check. Attendez-vous à Checked 0 audit events: 0 with a token or a secret. (0 événement d’audit vérifié : 0 avec un jeton ou un secret).

  2. Exécutez les nouveaux tests :

    Fenêtre de terminal
    python -m pytest tests/test_auditability.py
    tests/test_auditability.py ..... [100%]
    ============================== 5 passed in 1.11s ===============================

    Les cinq tests vérifient le refus d’un membre, la tentative d’une personne externe sans organisation et un jeton refusé sans le jeton. Un test exécute une connexion, une invitation et une déconnexion complètes. Il cherche ensuite un jeton, un témoin (cookie), un secret client ou un code d’invitation dans la piste et le journal. Il n’en trouve aucun. Le dernier vérifie le déclencheur d’ajout seulement.

  3. Exécutez tous les tests : python -m pytest. Attendez-vous à 452 passed, 1 deselected (452 réussis, 1 non sélectionné).

Chaque refus écrit maintenant une ligne, dans sa propre transaction. Cela ajoute un aller-retour vers la base de données sur un chemin auparavant peu coûteux. Une personne ou un script qui envoie beaucoup de mauvaises requêtes crée beaucoup de lignes. La piste grossit indéfiniment, car rien ne la supprime. Un vrai système a besoin d’une règle de conservation (par exemple, 13 mois) et d’un moyen d’archiver. Ce choix reste ouvert pour Remise du travail. Les lectures permises ne sont pas consignées ici. Répondre à « Qui a lu ce billet? » exigerait beaucoup plus de lignes. Consignez les lectures permises seulement lorsqu’une loi ou un contrat l’exige, par exemple pour l’accès de soutien.

Erreur courante

Le script ne trouve pas la piste

Symptôme : python -m scripts.audit n’affiche rien, mais vous venez d’envoyer des requêtes refusées.

Cause : l’API et le script utilisent des bases de données différentes, ou vous avez exécuté python -m scripts.load --reset après les requêtes. La réinitialisation vide la piste.

Solution : vérifiez que .env contient le même DATABASE_URL pour les deux, et envoyez les requêtes après la réinitialisation.

Refaites les tentatives après une réinitialisation, puis envoyez de nouveau la requête de Sam pour la piste d’audit de Larkfield. Avant d’exécuter python -m scripts.audit --summary, notez les lignes attendues : chaque action, son résultat et le nombre.

Afficher le corrigé
audit.read denied 1
ticket.read denied 2
ticket.write denied 2

Deux personnes externes ont tenté de lire (Sam chez Bramble Books, Tomás chez Larkfield). Deux membres avec accès en lecture seule ont tenté d’écrire (Omar, Camille). Sam a aussi demandé la piste d’audit. Les six tentatives autorisées et les deux identifiants devinés ne produisent aucune ligne.

Vérifiez votre compréhension

Sam (sans adhésion chez Bramble Books) demande le billet de soutien T-40001 de Bramble. Omar (accès en lecture seule chez Larkfield) tente d'ajouter un message chez Larkfield. Quel événement contient une organisation, et pourquoi l'autre n'en contient-il pas?

Afficher la réponse

L’événement d’Omar contient le locataire larkfield. Omar est membre, le code 403 ne lui apprend rien de nouveau. La propriétaire de Larkfield doit voir ce que ses membres tentent de faire. L’événement de Sam ne contient aucune organisation (NULL), et ses détails contiennent le chemin. Dans la piste d’audit de Bramble, il révélerait à sa propriétaire qu’une personne de Larkfield existe. Le code 404 cache cette information. L’équipe de la plateforme voit cet événement.

Vérifiez votre compréhension

Nommez trois éléments à ne jamais inclure dans un événement d'audit. Dites ce que l'événement conserve à la place.

Afficher la réponse

Jamais de jeton, de témoin (cookie), de secret client, de code d’invitation, de mot de passe ou de texte de message. L’événement conserve plutôt l’identifiant utilisateur de l’acteur, l’organisation, l’action et la cible (méthode et chemin). Il conserve aussi le résultat, la raison (par exemple, expiration ou role_cannot) et l’identifiant de la requête.

Vérifiez votre compréhension

Prédisez le résultat : Sam devine 50 identifiants de billets de Bramble en utilisant son propre chemin Larkfield. Combien d'événements d'audit cela produit-il, et où pouvez-vous voir les tentatives?

Afficher la réponse

Aucun. Sam passe les vérifications d’identité, d’adhésion et de rôle chez Larkfield. Chaque recherche répond 404 « n’existe pas », ce qui n’est pas enregistré. Les 50 tentatives apparaissent seulement dans le journal de l’API, sous forme de lignes 404. Pour les voir, comptez les réponses 404 par personne dans le journal.

Vérifiez votre compréhension

Vérification pratique : après les tentatives, qu'affichent python -m scripts.audit --summary et --check?

Afficher la réponse

--summary affiche une ligne par action et résultat : audit.read denied 1, ticket.read denied 2, ticket.write denied 2 après les tentatives et la requête de Sam. --check affiche Checked 5 audit events: 0 with a token or a secret. (5 événements d’audit vérifiés : aucun ne contient de jeton ou de secret) et se termine avec le code 0. Si un événement contient un jeton, il indique cet événement et se termine avec le code 1.

Récapitulatif

  • Un événement d’audit enregistre une décision : acteur, organisation, action, cible, résultat, raison, identifiant de la requête. La piste d’audit répond aux questions « qui a fait quoi? » et « qui a essayé? ».
  • Chaque refus produit maintenant un événement : token.check (vérification du jeton, sans acteur), not_a_member (non-membre, sans organisation, chemin dans les détails), role_cannot (rôle non autorisé, avec l’organisation).
  • La tentative d’une personne externe n’apparaît jamais dans la piste d’audit d’une autre organisation. Un identifiant deviné par son propre chemin ne produit aucun événement, seulement une ligne 404 dans le journal.
  • Jamais de secret : audit.write() nettoie les détails, et python -m scripts.audit --check cherche les jetons et les secrets de .env. La piste permet seulement des ajouts. Un jeton divulgué y reste donc.
  • Prochaine étape : remettre la conception, avec les preuves.

Leçon suivante : Remise du travail