Utilisateurs et organisations
- Leçon 2
- 55 min
- Module 1 : Bases de l'identité et des autorisations
Leçon 2 sur 19 · Authentification, autorisations et applications multilocatairesUtilisateurs et organisations
1. Bases de l'identité et des autorisations
2. Comprendre la connexion et les jetons
3. Sessions et intégration à l'application
4. Contrôler l'accès aux ressources
5. Travail en arrière-plan et administration
6. Valider et documenter la conception
Ce que vous saurez faire
Enregistrez dans PostgreSQL qui appartient à quelle organisation, et avec quel rôle. Ajoutez Bramble Books comme deuxième organisation. Notez ce que chaque rôle peut faire : c’est la matrice d’accès.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les trois questions de chaque requête, vues dans Trois questions distinctes.
- Les clés primaires, les clés étrangères et les règles
CHECK(vérification), vues dans Contraintes d’intégrité du cours sur les bases de données, et les migrations numérotées, vues dans Migrations.
Votre projet
~/projects/ticket-apide la dernière leçon (l’instantanéstart), avec PostgreSQL et Azurite en marche et l’environnement virtuel actif.
Cette leçon ajoute deux migrations, les données de la deuxième boutique, les personnes, ticket_api/access.py et son script. Après cette leçon, votre projet correspond à l’instantané end-of-m01.
Deux clubs, une personne
Section intitulée « Deux clubs, une personne »Imaginez une personne qui appartient à deux clubs. Au club d’échecs, elle est trésorière : elle peut payer les factures et modifier la liste des membres. Au club de course, elle est membre ordinaire : elle peut participer aux courses, mais pas dépenser l’argent du club.
C’est la même personne, avec un seul nom et un seul visage. Ce qu’elle peut faire dépend du club. Ses pouvoirs ne figurent donc pas sur sa carte d’identité. Chaque club les inscrit dans sa propre liste de membres, à côté de son nom.
Cette liste comporte trois parties : la personne, le club et son rôle dans ce club. L’API de billets de soutien a besoin d’exactement la même liste.
Le problème : une personne, deux rôles
Section intitulée « Le problème : une personne, deux rôles »La première idée d’Amira est d’ajouter une colonne role à une table de personnes. Chaque personne a une ligne :
| user_id | name | role |
|---|---|---|
usr-grace |
Grace | propriétaire |
usr-sam |
Sam | personnel |
usr-camille |
Camille | personnel? lecture seule? |
Cette table est un cas construit : elle ne fait pas partie du projet. Elle ne fonctionne plus pour Camille. Camille fait partie du personnel chez Larkfield et a un accès en lecture seule chez Bramble Books. Une colonne contient une seule valeur. La table donne donc forcément un résultat incorrect pour l’une des deux boutiques. Si elle indique staff (personnel), Camille peut modifier les billets de Bramble. Si elle indique read_only (lecture seule), Camille ne peut pas faire son travail chez Larkfield.
Une deuxième colonne, shop, n’aide pas non plus. Camille travaille dans deux boutiques, alors une seule ligne ne peut pas nommer les deux. Et une personne sans boutique, comme Tomás, n’a aucune valeur correcte.
Le rôle est un fait sur une personne dans une organisation, pas sur la personne. Il lui faut donc sa propre table.
L’idée : utilisateurs, locataires et adhésions
Section intitulée « L’idée : utilisateurs, locataires et adhésions »Le modèle comporte trois tables, une pour chaque idée :
- Un utilisateur est une personne qui peut se connecter. La clé est un identifiant d’utilisateur stable, comme
usr-camille. Ce n’est pas l’adresse courriel, car une adresse peut changer et deux services de connexion peuvent fournir la même. À partir du module 2, le fournisseur d’identité fournit l’identifiant d’utilisateur. - Un locataire est une organisation :
larkfieldoubramble. Dans ce cours, « locataire » et « organisation » désignent la même chose. - Une adhésion indique qu’un utilisateur appartient à un locataire, avec un rôle. La paire (locataire, utilisateur) est la clé primaire. Une personne a donc au plus une adhésion dans chaque organisation.
Un rôle nomme un ensemble d’actions permises. Le service de soutien a trois rôles : owner (propriétaire) gère les membres et fait tout le reste. staff lit et modifie les billets, et read_only permet seulement de les lire. Décider selon les rôles porte un nom : contrôle d’accès basé sur les rôles, ou RBAC. Vous ne listez pas les autorisations de Sam une par une. Vous dites « Sam fait partie du personnel », et le rôle personnel contient la liste.
Description textuelle du diagramme
Quatre tables. users contient une ligne par personne, avec l’identifiant d’utilisateur comme clé. tenants contient une ligne par organisation. memberships les relie : sa clé est la paire tenant_id et user_id, deux clés étrangères, et elle contient le role. Un utilisateur peut avoir zéro, une ou plusieurs adhésions. Un locataire peut avoir plusieurs adhésions et plusieurs invitations. Une invitation précise un locataire, une adresse courriel et un rôle, et elle expire.
La quatrième table, invitations, répond à une question pratique : comment une nouvelle personne rejoint-elle l’organisation? Un propriétaire invite une adresse courriel en précisant un rôle. La personne reçoit un code à usage unique qui expire. Lorsqu’elle l’accepte, une ligne d’adhésion apparaît. La table conserve seulement une empreinte du code (son SHA-256, expliqué dans Flux d’application). Ainsi, une copie de la base de données ne révèle pas les codes. Le module 5 utilise les invitations et en laisse une expirer.
Un petit exemple détaillé
Section intitulée « Un petit exemple détaillé »Voici les six adhésions du cours, et deux personnes qui n’en ont aucune :
| tenant_id | user_id | role |
|---|---|---|
larkfield |
usr-grace |
propriétaire |
larkfield |
usr-sam |
personnel |
larkfield |
usr-omar |
lecture seule |
larkfield |
usr-camille |
personnel |
bramble |
usr-ines |
propriétaire |
bramble |
usr-camille |
lecture seule |
Camille a deux lignes : deux organisations, deux rôles. Tomás et Kwame n’ont aucune ligne. Tomás peut se connecter à partir du module 2. Mais il n’appartient à aucune boutique, alors il ne voit rien. Kwame est un administrateur de la plateforme : il exploite le service pour tout le monde. Ce n’est pas un rôle de boutique, et ce rôle seul ne donne accès aux billets d’aucune boutique. Le module 5 lui donne un accès encadré et audité.
Description textuelle du diagramme
Larkfield a quatre membres : Grace comme propriétaire, Sam et Camille comme personnel, et Omar en lecture seule. Bramble Books en a deux : Ines comme propriétaire et Camille en lecture seule. Camille est la seule personne qui a deux adhésions, et ses deux rôles diffèrent. Tomás et Kwame n’ont aucune adhésion, donc aucune ligne.
Ajouter Bramble Books à la base de données
Section intitulée « Ajouter Bramble Books à la base de données »Construisez maintenant ce modèle. Les migrations et les données viennent de l’instantané end-of-m01.
-
Arrêtez l’API si elle est en marche. Dans
~/projects/ticket-api, copiez les fichiers de cette leçon depuis l’instantané. La copie inclut aussiscripts/attempts.py, que vous exécuterez dans la prochaine leçon.Fenêtre de terminal S=../nextia-learning-labs/C23/snapshots/end-of-m01for d in data docs migrations scripts tests ticket_api; do cp -R "$S/$d/." "$d/"; donecp "$S/README.md" .Fenêtre de terminal $S = "..\nextia-learning-labs\C23\snapshots\end-of-m01"foreach ($d in "data","docs","migrations","scripts","tests","ticket_api") { Copy-Item -Recurse -Force "$S\$d\*" $d }Copy-Item -Force "$S\README.md" . -
Ouvrez
migrations/013_tenants_and_memberships.sqldans VS Code. Repérez les quatre tables du diagramme. Repérez la règleCHECK (role IN ('owner', 'staff', 'read_only')): la base de données refuse tout autre rôle. La dernière ligne ajoute Larkfield comme premier locataire. -
Appliquez les migrations et chargez les deux boutiques. Utilisez
--reset, car votre base de données contient déjà les données de Larkfield :Fenêtre de terminal python -m scripts.load --resetapplied 013_tenants_and_membershipsapplied 014_tenant_columnsDatabase is up to date (14 migrations).Loaded the small data in 1.5 s:larkfield bramblecustomers 40 12tickets 200 40messages 331 58attachments 43 14documents 36 3ai_runs 294 69ticket_embeddings 200 4057 files uploaded to the object storage.3 messages have no ticket: they are in orphaned_messages. -
Exécutez les tests :
Fenêtre de terminal python -m pytestLa dernière ligne est
154 passed, 1 deselected(154 réussis, 1 désélectionné) : 46 de plus qu’avant. La plupart des nouveaux tests vérifient la matrice d’accès et les deux boutiques.
Bramble Books, sa clientèle et toutes les personnes de ce cours sont fictives. Un programme, data/build_bramble.py, a créé les 40 billets de la librairie (T-40001 à T-40040, du 26 septembre au 8 octobre 2026), ses photos et ses reçus. Les adresses courriel des personnes se terminent par .example, un nom qu’aucune adresse réelle ne peut avoir.
Examiner les personnes dans psql
Section intitulée « Examiner les personnes dans psql »Ouvrez psql avec docker compose exec db psql -U tickets -d tickets. D’abord, les deux locataires :
SELECT * FROM tenants; tenant_id | name | status | created_at-----------+---------------+--------+------------------------ larkfield | Larkfield | active | 2024-01-08 09:00:00+00 bramble | Bramble Books | active | 2026-09-15 09:00:00+00(2 rows)Maintenant, les adhésions de Camille, avec une jointure pour ajouter les noms :
SELECT u.name, t.name AS organization, m.role FROM memberships m JOIN users u USING (user_id) JOIN tenants t USING (tenant_id) WHERE m.user_id = 'usr-camille'; name | organization | role---------+---------------+----------- Camille | Larkfield | staff Camille | Bramble Books | read_only(2 rows)Et les deux personnes sans adhésion. Un LEFT JOIN (jointure externe gauche) conserve un utilisateur même sans adhésion correspondante et laisse les colonnes manquantes vides :
SELECT u.name, m.tenant_id, m.role FROM users u LEFT JOIN memberships m USING (user_id) WHERE u.user_id IN ('usr-tomas', 'usr-kwame'); name | tenant_id | role-------+-----------+------ Tomás | | Kwame | |(2 rows)Chaque ligne appartient à une organisation
Section intitulée « Chaque ligne appartient à une organisation »Les utilisateurs et les adhésions indiquent « qui appartient où ». Les données du service de soutien ont besoin de la même réponse : à qui appartient ce billet? La migration 014_tenant_columns.sql ajoute une colonne tenant_id à chaque table du service de soutien : clients, billets, messages, pièces jointes, exécutions d’IA, vecteurs et documents. Les lignes existantes reçoivent larkfield. Ensuite, la colonne n’a aucune valeur par défaut : chaque nouvelle ligne doit préciser à quelle organisation elle appartient.
SELECT tenant_id, count(*) FROM tickets GROUP BY 1; tenant_id | count-----------+------- bramble | 40 larkfield | 200(2 rows)La migration modifie aussi les clés étrangères. Avant, un message pointait vers un billet seulement par ticket_id. Maintenant, il pointe vers la paire (tenant_id, ticket_id). Un message peut donc seulement appartenir à un billet de la même organisation. Essayez de rattacher un message de Larkfield au billet T-40001 de Bramble :
INSERT INTO messages (tenant_id, ticket_id, author, body) VALUES ('larkfield', 'T-40001', 'agent', 'x');ERROR: insert or update on table "messages" violates foreign key constraint "messages_ticket_fk"DETAIL: Key (tenant_id, ticket_id)=(larkfield, T-40001) is not present in table "tickets".T-40001 existe, mais pas comme billet de Larkfield. La base de données refuse le lien, peu importe ce que fait le code de l’application. C’est un deuxième verrou derrière les vérifications de l’API. Le module 4 en ajoute d’autres.
Deux règles auparavant globales s’appliquent maintenant par organisation. Une cliente, Olga Olsen, achète dans les deux boutiques avec la même adresse courriel. Le cours sur les bases de données permettait un compte par adresse. Maintenant, c’est un compte par adresse dans chaque boutique :
SELECT tenant_id, customer_id, name, email FROM customers WHERE lower(email) = 'olga.olsen@example.com'; tenant_id | customer_id | name | email-----------+-------------+------------+------------------------ larkfield | C-0001 | Olga Olsen | olga.olsen@example.com bramble | B-20105 | Olga Olsen | olga.olsen@example.com(2 rows)Les documents suivent la même règle : les deux boutiques ont un returns-policy, et chacune a le sien. Tapez \q pour quitter psql.
La matrice d’accès
Section intitulée « La matrice d’accès »Les tables indiquent le rôle de chaque personne. Notez maintenant ce que chaque rôle peut faire. Une matrice d’accès est une table avec une ligne par action et une colonne par rôle. Chaque cellule indique « permis » ou « refusé ». Le projet la conserve sous deux formes qui doivent concorder : en code dans ticket_api/access.py, et par écrit dans docs/access-matrix.md.
python -m scripts.access_matrixWhat each role may do (yes = allowed): action owner staff read_only ticket.read yes yes yes ticket.write yes yes 403 file.read yes yes yes file.upload yes yes 403 document.read yes yes yes document.read_staff yes yes 403 member.read yes yes 403 member.manage yes 403 403 export.create yes yes 403 audit.read yes 403 403Le script affiche ensuite chaque personne dans chaque boutique. Cette page montre seulement la première partie. Chaque action nomme une chose et un verbe : ticket.write signifie « ajouter un message à un billet ». Et document.read_staff signifie « lire aussi les documents réservés au personnel ».
Les cellules qui indiquent 403 sont des refus. Il existe un deuxième type de refus. Une personne sans adhésion reçoit 404 pour chaque action. La fonction qui décide est courte. Ouvrez ticket_api/access.py et repérez decide() :
if role is None: return Decision(False, 404, "not_a_member", "no membership in this organization")if action not in ROLE_ACTIONS.get(role, frozenset()): return Decision(False, 403, "role_cannot", f"{role} may not {action}")Pourquoi deux codes d’état? Un 403 Forbidden (interdit) dit « ceci existe, mais vous ne pouvez pas faire cela ». C’est acceptable pour un membre : Omar peut déjà lire les billets de Larkfield. Un refus d’écriture ne lui apprend donc rien de nouveau. Pour une personne externe, même « ceci existe » est une information. Sam ne devrait pas apprendre que Bramble Books a un billet T-40001. Une personne externe reçoit donc 404 Not Found (introuvable), comme pour quelque chose qui n’existe pas. L’autorisation dans l’API intègre cela à chaque route.
decide() n’utilise ni base de données ni requête Web. Elle prend un rôle et une action, puis renvoie une décision. Cela rend la matrice facile à lire, à tester et à afficher. Essayez-la pour trois cas :
python -c "from ticket_api.access import decide; print(decide('staff', 'ticket.write')); print(decide('read_only', 'ticket.write')); print(decide(None, 'ticket.read'))"Decision(allowed=True, status=200, code='allowed', rule='staff may ticket.write')Decision(allowed=False, status=403, code='role_cannot', rule='read_only may not ticket.write')Decision(allowed=False, status=404, code='not_a_member', rule='no membership in this organization')Combinez maintenant les deux parties : une personne, une organisation et une action. L’adhésion de la personne dans cette organisation donne le rôle, et le rôle donne la réponse.
Explorer
Qui peut faire quoi, et où
Prédisez d’abord : Camille fait partie du personnel chez Larkfield et peut donc ajouter un message à un billet de Larkfield. Prédisez la réponse lorsque Camille ajoute un message à un billet de Bramble Books. Et lorsque Tomás lit un billet de Larkfield?
- Rôle dans cette organisation
- staff
- L’API répond
- 200 allowed
- La règle
- staff may ticket.write
Camille a le rôle staff chez Larkfield, et la règle dit « staff may ticket.write ». Permis : l’API fait l’action.
- 200 : permis
- 403 : un membre, mais le rôle ne le permet pas
- 404 : pas un membre, rien n’est montré
| Action | Larkfield staff | Bramble Books read_only |
|---|---|---|
ticket.readlister et lire les billets, trouver des billets semblables | 200 oui | 200 oui |
ticket.writeajouter un message à un billet | 200 oui (choisi) | 403 |
file.readobtenir un lien de téléchargement pour une pièce jointe | 200 oui | 200 oui |
file.uploadtéléverser une pièce jointe | 200 oui | 403 |
document.readchercher dans les documents d’aide publics | 200 oui | 200 oui |
document.read_staffaussi les documents réservés au personnel | 200 oui | 403 |
member.readlister les membres de l’organisation | 200 oui | 403 |
member.manageinviter, changer un rôle, retirer un membre | 403 | 403 |
export.createlancer une exportation des billets en arrière-plan | 200 oui | 403 |
audit.readlire les événements d’audit de l’organisation | 403 | 403 |
Chaque réponse vient de la fonction decide() du projet, dans ticket_api/access.py, affichée par python -m scripts.access_matrix --json. « 200 » veut dire permis : une requête qui crée un enregistrement, comme un nouveau message, répond 201. Les personnes sont fictives.
Description textuelle de l’explorateur
L’explorateur a trois groupes de boutons : une personne, une organisation et une action. Il montre le rôle de la personne dans cette organisation, la réponse de l’API (200, 403 ou 404) et la règle appliquée. Une table montre toute la ligne de la personne dans la matrice : les dix actions dans les deux organisations. La cellule choisie est marquée.
Il commence avec Camille, Larkfield, ticket.write : Camille y fait partie du personnel. La règle dit « staff peut faire ticket.write », et la réponse est 200. Chez Bramble Books, Camille a le rôle read_only. La règle dit « read_only ne peut pas faire ticket.write », et la réponse est 403. Tomás n’a aucune adhésion dans les deux organisations, alors chaque action dans les deux organisations renvoie 404. Ines reçoit 200 pour chaque action chez Bramble Books et 404 pour chaque action chez Larkfield.
Les deux réponses de Camille à une même action viennent de deux adhésions, pas de deux personnes. C’est la raison d’être de la table des adhésions.
À vous : rédiger une matrice d’accès
Section intitulée « À vous : rédiger une matrice d’accès »Ines demande deux nouvelles actions chez Bramble Books. Rédigez leurs lignes pour owner, staff et read_only, avec « oui » ou « 403 » dans chaque cellule. Donnez une raison par ligne. Notez ensuite ce que Tomás reçoit.
ticket.close: fermer un billet lorsque le problème est résolu.settings.change: modifier le nom de la boutique et ses heures d’ouverture sur la page d’aide.
Rédigez vos lignes avant d’ouvrir la réponse dans la vérification des connaissances ci-dessous. Il n’y a aucun code à modifier dans cette tâche.
Quand utiliser les rôles, et ce qu’ils coûtent
Section intitulée « Quand utiliser les rôles, et ce qu’ils coûtent »Les rôles fonctionnent bien lorsque les personnes se répartissent en quelques groupes qui font le même travail. Trois rôles et dix actions tiennent sur un écran. Une nouvelle personne a besoin d’une ligne d’adhésion, pas de dix autorisations.
Les rôles ont des limites. Une règle comme « Sam peut fermer seulement ses propres billets » dépend de l’enregistrement, pas seulement du rôle. Un rôle ne peut pas l’exprimer, alors le code doit aussi vérifier l’enregistrement. Quand ces règles se multiplient, les équipes ajoutent des règles basées sur les attributs aux rôles. Ce cours utilise seulement les rôles, plus l’organisation de chaque enregistrement.
Le modèle a aussi des coûts. Chaque requête doit lire la table des adhésions, car un rôle peut changer à tout moment. Le module 5 mesure ce qui arrive lorsque l’API conserve une ancienne réponse pendant 30 secondes. Et chaque table a besoin de son tenant_id, avec des clés et des index qui l’incluent. Vous avez vu le début de cela dans la migration 014.
Erreurs courantes
Section intitulée « Erreurs courantes »Erreur courante
Le chargement s'arrête : les tables contiennent déjà des données
Symptôme : python -m scripts.load affiche seulement cette ligne, et les tests échouent parce que la colonne tenant_id manque :
The help-desk tables already have data. Run again with --reset to delete it and load again.Cause : le programme de chargement refuse de charger des données par-dessus les données existantes. Il s’arrête avant d’appliquer les nouvelles migrations.
Solution : exécutez python -m scripts.load --reset. Cette commande applique 013 et 014, et recharge les deux boutiques.
Erreur courante
access_matrix : aucune personne avec cet identifiant
Symptôme : python -m scripts.access_matrix --user camille affiche No person with the ID camille. (aucune personne avec l’identifiant camille).
Cause : l’option attend l’identifiant d’utilisateur, pas le nom.
Solution : utilisez l’identifiant d’utilisateur de la table users : --user usr-camille.
Réinitialisation. python -m scripts.load --reset recharge les deux boutiques et les personnes. Si votre projet ne fonctionne plus, renommez-le et copiez l’instantané end-of-m01 avec votre .env, comme à l’étape 2 de la dernière leçon.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Pourquoi le rôle est-il une colonne des adhésions plutôt que des utilisateurs? Utilisez Camille dans votre réponse.
Afficher la réponse
Un rôle indique ce qu’une personne peut faire dans une organisation. Camille est membre du personnel chez Larkfield et a un accès en lecture seule chez Bramble Books. Une colonne role (rôle) dans users (utilisateurs) contient une seule valeur. Elle serait donc incorrecte pour l’un des deux magasins. Il existe une ligne d’adhésion par personne et par organisation. Chacune des deux adhésions de Camille porte donc son propre rôle.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Votre matrice d'accès : écrivez les lignes pour ticket.close et settings.change (owner, staff, read_only), avec une raison pour chaque ligne. Qu'obtient Tomás?
Afficher la réponse
Une bonne réponse :
| Action | owner | staff | read_only | Raison |
|---|---|---|---|---|
ticket.close (fermer un billet) |
oui | oui | 403 | La fermeture modifie un billet. Le personnel modifie les billets; les membres en lecture seule ne font que les lire. |
settings.change (modifier les paramètres) |
oui | 403 | 403 | Cette action modifie tout le magasin. Seul le propriétaire gère le magasin, comme avec member.manage (gérer les membres). |
Tomás n’a aucune adhésion chez Bramble Books. Il obtient donc 404 pour les deux actions, comme pour toute autre action. Vos raisons peuvent différer. Vérifiez qu’aucune ligne ne permet à read_only (lecture seule) de modifier quoi que ce soit. Vérifiez aussi qu’une personne sans adhésion n’obtient jamais 403.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Prédisez : la base de données contient les billets T-30002 (larkfield) et T-40001 (bramble). Que se passe-t-il avec INSERT INTO messages (tenant_id, ticket_id, author, body) VALUES ('bramble', 'T-30002', 'agent', 'Hi'), et pourquoi?
Afficher la réponse
La base de données refuse l’insertion avec une erreur ForeignKeyViolation (violation de clé étrangère) sur messages_ticket_fk : Key (tenant_id, ticket_id)=(bramble, T-30002) is not present in table "tickets". (La paire de clés (bramble, T-30002) est absente de la table « tickets ».) La clé étrangère utilise la paire, et T-30002 est un billet de Larkfield. Un message peut seulement appartenir à un billet de sa propre organisation.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Un utilisateur est une personne, identifiée par un identifiant d’utilisateur stable. Un locataire est une organisation. Une adhésion les relie et contient le rôle.
- Camille a deux adhésions avec deux rôles : membre du personnel chez Larkfield, accès en lecture seule chez Bramble Books. Tomás et Kwame n’en ont aucune.
- Chaque ligne du service de soutien contient un
tenant_id(identifiant de locataire). Les clés étrangères utilisent (tenant_id,ticket_id) : la base de données refuse un lien entre magasins. - La matrice d’accès indique ce que
owner(propriétaire),staff(personnel) etread_onlypeuvent faire.decide()renvoie 200, 403 pour un membre dont le rôle interdit l’action, et 404 pour une personne sans adhésion. - Votre projet correspond à
end-of-m01:154 passed, 1 deselected(154 tests réussis, 1 test exclu).
Ensuite, vous attaquez votre propre API d’exercice avec des tentatives inventées. Vous verrez ce que la clé partagée laisse passer.
Leçon suivante : Exemples de menaces