Tests négatifs
- Leçon 17
- 55 min
- Module 6 : Valider et documenter la conception
Leçon 17 sur 19 · Authentification, autorisations et applications multilocatairesTests négatifs
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
Testez ce qui doit échouer, pas seulement ce qui doit fonctionner. Exécutez une matrice de tests qui envoie chaque personne, dans chaque organisation, vers chaque route protégée et vérifie le refus exact. Ajoutez les mauvaises identités et les identifiants d’enregistrement de l’autre boutique. Puis brisez volontairement l’API et regardez la matrice détecter le problème.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- La matrice d’accès et ses règles, 404 pour les personnes externes et 403 pour les membres, présentées dans Utilisateurs et organisations et L’autorisation dans l’API.
- Les 15 mauvais jetons inventés et les vérifications qui les refusent, présentés dans Ce que signifie un jeton.
pytestet son marqueurparametrize, présentés dans Créer et déployer votre première API.
Ce qu’il vous faut sur votre ordinateur
- Le projet après Changements de cycle de vie : il correspond à l’instantané
end-of-m05, etpython -m pytestaffiche234 passed, 1 deselected. - PostgreSQL et Azurite en marche, avec les données des deux boutiques chargées. Les tests démarrent le fournisseur d’exercice dans le processus de test. Vous n’avez donc pas besoin d’un terminal pour le fournisseur dans cette leçon.
Chaque bogue de cette leçon est un cas construit : écrit volontairement, exécuté, puis retiré. Testé sur macOS (Apple silicon); les commandes Windows sont écrites, mais non testées.
La ronde de nuit dans un musée
Section intitulée « La ronde de nuit dans un musée »Un musée ferme à six heures. Le gardien de nuit ne vérifie pas seulement que la clé du personnel ouvre la porte principale. Il parcourt tout le bâtiment. Il essaie chaque porte avec la mauvaise clé. Il essaie d’ouvrir la réserve avec un billet de visiteur. Il pousse la sortie de secours depuis l’extérieur. Son travail consiste à trouver une porte qui s’ouvre alors qu’elle devrait rester fermée.
La plupart des tests ressemblent au personnel de jour : ils vérifient que la bonne clé ouvre la bonne porte. Le gardien vérifie l’inverse. Dans cette leçon, vous faites une ronde de nuit dans l’API de billets.
Le problème : 234 tests réussis et une faille
Section intitulée « Le problème : 234 tests réussis et une faille »Votre projet compte 234 tests. Plusieurs vérifient l’accès : Omar ne peut pas ajouter de message, Sam reçoit 404 pour le billet de Bramble, Camille reçoit deux réponses. Chaque test a été écrit à la main, pour le cas enseigné dans la leçon.
Faites maintenant une petite erreur volontaire. Dans ticket_api/members.py, la route qui liste les membres demande l’action member.read. Remplacez-la par ticket.read :
def members(caller: Annotated[Caller, Depends(require("ticket.read"))], db: DB) -> MemberList:Un développeur pourrait faire cette erreur en copiant une route de tickets.py. Exécutez tous les tests :
python -m pytest -qLa dernière ligne :
234 passed, 1 deselected in 20.62sTous les tests réussissent. Mais Omar, qui a un accès en lecture seule, peut maintenant lister les membres de Larkfield : identifiants d’utilisateur, noms, rôles et dates d’arrivée. Camille peut lister les membres de Bramble, où son accès est aussi en lecture seule. Aucun test ne l’a vérifié, car aucun n’a envoyé une personne en lecture seule vers la liste des membres. Les tests choisis à la main couvrent les cas auxquels quelqu’un a pensé. Une élévation de privilèges se cache dans un cas auquel personne n’a pensé. Laissez le bogue pour le moment : vous le réutiliserez dans quelques minutes.
L’idée : tester toute la matrice
Section intitulée « L’idée : tester toute la matrice »La matrice d’accès indique déjà ce que chaque personne peut faire dans chaque organisation. Les tests n’ont donc pas besoin de choisir des cas. Ils peuvent envoyer chaque personne vers chaque route dans chaque organisation et comparer la réponse à la matrice. C’est une matrice de tests : un test paramétré, avec un cas de test par cellule.
Tester ce qui doit échouer porte un nom : les tests négatifs. Un test positif vérifie que Grace peut inviter une personne. Un test négatif vérifie que Sam ne le peut pas et qu’il reçoit le bon refus : 403, pas 500 ni 201. Pour le contrôle d’accès, les tests négatifs comptent plus que les tests positifs. Une autorisation manquante dérange un utilisateur, qui la signale aussitôt. Une autorisation en trop cause du tort sans se faire remarquer.
La matrice de tests du cours comprend trois parties :
Description textuelle du diagramme
Le fichier de tests comprend trois groupes. D’abord, 7 personnes fois 2 organisations fois 11 routes : 154 vérifications. Ensuite, 4 cas où un membre utilise le bon chemin avec l’identifiant d’enregistrement de l’autre organisation; chaque cas envoie 3 requêtes. Enfin, 5 types de mauvaise identité sur chacune des 11 routes : 55 vérifications. Au total : 213.
Chaque groupe teste certains des cinq types d’échec :
| Type d’échec | Où dans la matrice | Réponse nécessaire |
|---|---|---|
| Identité absente | mauvaise identité : no identity (aucune identité) |
401 |
| Identifiants expirés | mauvaise identité : expired (expiré) |
401 |
| Mauvaise audience | mauvaise identité : wrong audience (mauvaise audience), et un ID token (jeton d’identité; son audience est l’application) |
401 |
| Rôle insuffisant | les cellules des membres en lecture seule et du personnel; aussi no scope (aucune portée) |
403 |
| Mauvaise organisation | les cellules des personnes externes et l’identifiant d’enregistrement de l’autre boutique | 404 |
Une toute petite matrice pour commencer
Section intitulée « Une toute petite matrice pour commencer »Avant le grand fichier, regardez une ligne où toutes les valeurs sont visibles. Omar a un accès en lecture seule chez Larkfield. La matrice dans ticket_api/access.py donne trois actions à read_only : ticket.read, file.read et document.read. Donc, pour Omar chez Larkfield :
| Route | Action | Le rôle d’Omar l’autorise? | Résultat attendu |
|---|---|---|---|
| lire un billet | ticket.read |
oui | 200 |
| ajouter un message | ticket.write |
non | 403 |
| lister les membres | member.read |
non | 403 |
| lire le journal d’audit | audit.read |
non | 403 |
Chez Bramble Books, Omar n’a aucune adhésion. Chaque route doit donc renvoyer 404, avec le même corps que pour une organisation inexistante. Le test ne fixe pas ces nombres dans le code. Il demande la décision à decide() et la transforme en code d’état attendu :
def expected(user: str, tenant: str, route: str) -> int: action, *_, ok = ROUTES[route] decision = decide(MEMBERSHIPS.get((tenant, user)), action) return ok if decision.allowed else decision.statusok est le code d’état d’un appel réussi : 200 pour une lecture, 201 pour un nouveau message, 202 pour une exportation qui démarre. Ensuite, une fonction de test s’exécute pour chaque cellule. pytest.mark.parametrize crée 154 cas de test à partir d’une fonction et donne à chacun un identifiant lisible comme usr-omar|larkfield|upload a file (Omar chez Larkfield, téléverser un fichier).
Ajouter la matrice de tests
Section intitulée « Ajouter la matrice de tests »-
Copiez le fichier de tests depuis l’instantané
end-of-m06:Fenêtre de terminal S=../nextia-learning-labs/C23/snapshots/end-of-m06cp "$S/tests/test_access_matrix.py" tests/Fenêtre de terminal $S = "..\nextia-learning-labs\C23\snapshots\end-of-m06"Copy-Item "$S\tests\test_access_matrix.py" tests\ -
Votre API contient toujours le bogue de la liste des membres. Exécutez seulement le nouveau fichier :
Fenêtre de terminal python -m pytest tests/test_access_matrix.py -qE AssertionError: {"items":[{"user_id":"usr-grace","name":"Grace","role":"owner","since":"2024-01-08T09:00:00Z"},…E assert 200 == 403E + where 200 = <Response [200 OK]>.status_codeE + and 403 = expected('usr-omar', 'larkfield', 'list members')…FAILED tests/test_access_matrix.py::test_matrix[usr-omar|larkfield|list members] - AssertionError: …FAILED tests/test_access_matrix.py::test_matrix[usr-camille|bramble|list members] - AssertionError: …2 failed, 211 passed in 19.84sLa matrice trouve ce que 234 tests ont manqué. Chaque échec nomme la cellule, et l’assertion montre la réponse de l’API :
assert 200 == 403. La sortie est abrégée ici. Un code 200 signifie que l’API a envoyé la liste des membres à une personne en lecture seule. -
Retirez le bogue : remplacez
ticket.readparmember.readdansticket_api/members.py. Exécutez de nouveau la matrice :Fenêtre de terminal python -m pytest tests/test_access_matrix.pycollected 213 itemstests/test_access_matrix.py ............................................ [ 20%]........................................................................ [ 54%]........................................................................ [ 88%]......................... [100%]============================= 213 passed in 18.77s =============================La durée varie selon l’ordinateur. Chaque cas crée un nouveau client de test, donc les 213 cas prennent quelques secondes.
-
Exécutez les lignes d’une seule personne pour voir les identifiants de test :
Fenêtre de terminal python -m pytest tests/test_access_matrix.py -k "usr-omar" -vChaque ligne nomme une personne, une organisation et une route, et chaque ligne affiche
PASSED(réussi).
Lire la matrice comme une image
Section intitulée « Lire la matrice comme une image »Une liste de 213 noms de tests est difficile à lire. L’image ci-dessous présente les mêmes résultats dans une grille : une ligne par personne dans chaque organisation, une colonne par route. La couleur et le nombre d’une cellule indiquent le code d’état exigé par la matrice d’accès. Toutes les cellules ont réussi ici : l’API a répondu avec exactement ce code d’état.
- permis (200, 201, 202)
- 403 : membre, rôle insuffisant
- 404 : pas un membre
- 401 : aucune identité valide
Description textuelle du graphique
Quatorze lignes montrent les 7 personnes dans les 2 organisations, avec leur rôle dans chacune. Grace (propriétaire) et Ines (propriétaire) reçoivent 200, 201 ou 202 sur les 11 routes de leur propre organisation. Sam et Camille font partie du personnel chez Larkfield : ils reçoivent 403 pour « inviter une personne » et « lire le journal d’audit », et réussissent ailleurs. Omar chez Larkfield et Camille chez Bramble Books ont un accès en lecture seule. Ils réussissent pour lister les billets, lire un billet, consulter les billets semblables, télécharger un fichier et chercher des documents. Ils reçoivent 403 sur les six autres routes. Chaque ligne sans adhésion, dont celles de Tomás et Kwame dans les deux organisations, affiche 404 sur les 11 routes. Sous les personnes figurent cinq lignes de mauvaise identité, envoyées aux routes de Larkfield. Aucune identité, expiration, mauvaise audience et jeton d’identité donnent 401 sur les 11 routes. Un jeton sans portée reçoit 403 sur les 11 routes. Identifiant d’enregistrement de l’autre organisation : 4 cas sur 4 réussissent. 213 vérifications sur 213 ont réussi.
Lisez-la comme une carte. Les blocs gris représentent les personnes externes : des lignes entières de 404. Les blocs orange représentent les membres au rôle plus limité. Les blocs bleus représentent le travail prévu pour chaque rôle. Tomás et Kwame n’ont aucune adhésion, donc leurs deux lignes sont grises. Kwame est administrateur de la plateforme, mais ce n’est pas une adhésion : sans autorisation de soutien, il reçoit 404 comme tout le monde.
Voici maintenant la même image pour l’API avec le bogue de la liste des membres. Les données sont le résultat réel de l’exécution du cas construit :
- permis (200, 201, 202)
- 403 : membre, rôle insuffisant
- 404 : pas un membre
Description textuelle du graphique
Les mêmes 14 lignes et 11 colonnes. Deux cellules ont un contour épais : Omar chez Larkfield, lister les membres, résultat attendu 403; et Camille chez Bramble Books, lister les membres, résultat attendu 403. Dans les deux cellules, l’API a répondu 200. Toutes les autres cellules ont réussi. 211 vérifications sur 213 ont réussi.
Les deux cellules en échec se trouvent dans les lignes en lecture seule, dans une seule colonne. Cette disposition vous indique où chercher : une route et la vérification d’un rôle. Une cellule en échec dans une ligne grise indiquerait un bogue bien plus grave : une personne externe est entrée.
Les mauvaises identités et les identifiants de l’autre boutique
Section intitulée « Les mauvaises identités et les identifiants de l’autre boutique »Les deux derniers groupes utilisent la même idée. Cinq types de mauvaise identité sont envoyés à chaque route :
BAD = ["no identity", "expired", "wrong audience", "ID token", "no scope"]Les quatre premiers doivent recevoir 401, et le dernier 403 : la personne est connue, mais le jeton n’autorise pas l’action. Dans Ce que signifie un jeton, les 15 jetons inventés étaient envoyés à un seul point de terminaison, /v1/me. Ici, cinq d’entre eux sont envoyés aux 11 routes. Cela vérifie qu’aucune route n’a oublié la vérification d’identité, par exemple une nouvelle route sans require().
L’identifiant d’enregistrement de l’autre boutique correspond à l’attaque au niveau de l’objet présentée dans Exemples de menaces. Sam utilise son propre chemin Larkfield, mais avec le billet T-40001 de Bramble. Lire le billet et ses billets semblables doit donner 404. Ajouter un message doit aussi donner 404, ou 403 pour un membre en lecture seule, car le rôle est vérifié avant la recherche.
Qui vérifie les réponses attendues?
Section intitulée « Qui vérifie les réponses attendues? »Kwame pose une question gênante : « La matrice tire ses réponses attendues de decide(). Et si decide() se trompe? »
Essayez. Ajoutez un deuxième bogue construit dans ticket_api/access.py : donnez aussi l’action ticket.write à read_only. Puis exécutez la matrice et le test de politique :
python -m pytest tests/test_access_matrix.py tests/test_access_policy.py -qFAILED tests/test_access_matrix.py::test_another_organizations_record_id[usr-camille|bramble|T-30002]FAILED tests/test_access_policy.py::test_the_code_is_the_matrix[2-read_only-ticket.write]FAILED tests/test_access_policy.py::test_a_role_that_cannot_is_403_and_names_the_ruleFAILED tests/test_access_policy.py::test_camille_gets_two_answers_for_one_action4 failed, 244 passed in 19.18sLes lignes d’échec sont abrégées ici. Les 154 cellules de la matrice ont réussi : l’API et decide() concordaient, mais les deux se trompaient. Omar pouvait écrire, et la matrice attendait exactement cela. Une seule attente écrite à la main dans la matrice a détecté le problème, dans un cas avec l’identifiant de l’autre boutique. Trois tests de test_access_policy.py ont échoué. Ce fichier contient la matrice sur papier recopiée à la main, ligne par ligne, et la compare à decide().
Description textuelle du diagramme
Les règles commencent sur papier, dans docs/access-matrix.md. Le test de politique est une copie du papier écrite à la main, et il vérifie decide(). La matrice de tests demande chaque code d’état attendu à decide() et vérifie les 213 réponses de l’API. Un bogue dans une route fait échouer la matrice de tests. Un bogue dans decide() fait réussir les cellules de la matrice de tests et échouer le test de politique.
Vous avez donc besoin des deux. La matrice de tests trouve une route qui ne respecte pas les règles. Le test de politique trouve des règles qui ne correspondent pas aux règles convenues. Retirez le deuxième bogue avant de continuer.
Un troisième bogue construit : quand le deuxième verrou masque le premier
L’auteur a volontairement retiré l’organisation d’une requête : get_ticket() dans ticket_api/repository.py, sans t.tenant_id = %(tenant_id)s. La matrice de tests a quand même réussi, 213 sur 213. La sécurité au niveau des lignes présentée dans Requêtes sûres pour chaque locataire a tenu : sous ticket_app pour Larkfield, la base de données ne montrait aucune ligne de Bramble. Avec une copie de la matrice qui définit db_row_security=False, les 4 cas avec l’identifiant de l’autre boutique ont échoué avec assert 200 == 404. Le deuxième verrou masquait un premier verrou brisé. Une exécution des tests sans le deuxième verrou montre si le premier fonctionne encore.
Exécuter tous les tests
Section intitulée « Exécuter tous les tests »Exécutez encore une fois toute la suite, sans bogue dans le code :
python -m pytest -qLa dernière ligne :
447 passed, 1 deselected in 38.67sLa matrice prend quelques secondes à chaque exécution, et davantage lorsque vous ajoutez une route ou une personne : les cellules se multiplient. Cela reste peu coûteux. Une nouvelle route demande une ligne dans ROUTES et reçoit aussitôt 14 cellules et 5 mauvaises identités. Une nouvelle personne ou adhésion demande une ligne dans PEOPLE ou MEMBERSHIPS. Quand ne pas utiliser une matrice complète : pour les règles qui dépendent des données, pas du rôle. Par exemple : « L’auteur peut modifier son propre message pendant 5 minutes ». Testez ces règles avec quelques cas écrits à la main, à côté de la matrice.
Erreurs courantes
Section intitulée « Erreurs courantes »Erreur courante
La matrice réussit instantanément
Symptôme : python -m pytest tests/test_access_matrix.py affiche 213 skipped in 0.01s (213 tests ignorés en 0,01 seconde).
Cause : PostgreSQL n’est pas en marche. Les tests s’ignorent lorsqu’ils ne peuvent pas joindre la base de données. Un test ignoré n’est pas un test réussi.
Solution : démarrez la base de données (docker compose up -d db azurite) et exécutez de nouveau les tests. Ajoutez -rs pour voir pourquoi des tests ont été ignorés : PostgreSQL is not running (with Docker: docker compose up -d db) (PostgreSQL n’est pas en marche; avec Docker, utilisez la commande indiquée).
Erreur courante
Une nouvelle route n'a aucune cellule
Symptôme : vous ajoutez une route, et la matrice compte toujours 213 vérifications.
Cause : la matrice teste seulement les routes de sa table ROUTES. Une route absente de cette table n’est pas testée.
Solution : ajoutez chaque nouvelle route protégée à ROUTES, avec son action, sa méthode, son chemin, son corps et son code d’état de réussite. La révision d’une nouvelle route devrait poser la question : « Est-elle dans la matrice? »
À vous de jouer : prédire une ligne
Section intitulée « À vous de jouer : prédire une ligne »Camille fait partie du personnel chez Larkfield et a un accès en lecture seule chez Bramble Books. Sans regarder l’image, écrivez ses deux lignes : 11 codes d’état chacune. Ensuite, exécutez python -m pytest tests/test_access_matrix.py -k "usr-camille" -v et comparez.
Afficher la réponse
Chez Larkfield (personnel) : 200, 200, 200, 201, 200, 201, 200, 200, 403 (inviter une personne), 202, 403 (lire le journal d’audit). Chez Bramble Books (lecture seule) : 200, 200, 200, 403, 200, 403, 200, 403 (lister les membres), 403, 403, 403. Les 22 cellules affichent PASSED (réussi), tout comme ses deux cas utilisant l’identifiant de l’autre boutique.
Vérifiez votre compréhension
Nommez les cinq types d'échecs vérifiés par la matrice de tests, et le code d'état attendu pour chacun.
Afficher la réponse
Identité absente : 401. Identifiants expirés : 401. Mauvaise audience (y compris un jeton d’identité envoyé comme jeton d’accès) : 401. Rôle ou portée insuffisants : 403, car la personne est membre et voit déjà l’organisation. Mauvaise organisation : 404, pour une personne externe comme pour l’identifiant d’un enregistrement d’une autre organisation, afin de ne rien révéler.
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 le résultat : un développeur ajoute une route, DELETE /v1/tenants/{tenant}/tickets/{id}, et oublie require(). Il l'ajoute à ROUTES avec l'action ticket.write. Quelles cellules de la matrice échouent?
Afficher la réponse
Sans require(), la route ne vérifie ni l’identité ni l’adhésion. Les quatre mauvaises identités qui exigent 401 échouent, ainsi que la ligne sans portée qui exige 403. Chaque ligne de personne externe attend 404 et échoue (Grace, Sam et Omar chez Bramble Books, Ines chez Larkfield, Tomás et Kwame dans les deux). Les lignes en lecture seule attendent 403 et échouent. Seules les cellules des propriétaires et du personnel, qui peuvent écrire, réussissent.
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
Pourquoi le projet a-t-il besoin de test_access_policy.py alors que la matrice de tests contient déjà 213 vérifications?
Afficher la réponse
La matrice de tests tire ses réponses attendues de decide(). Si decide() se trompe, l’API et la réponse attendue se trompent ensemble, et les cellules de la matrice réussissent. Quand read_only autorisait l’écriture, les 154 cellules réussissaient. Le test de la politique compare decide() à une copie écrite à la main de la matrice papier. Il détecte donc une règle incorrecte.
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
Vérification pratique : exécutez la matrice de tests. Combien de vérifications réussissent, et que signifie une vérification ignorée?
Afficher la réponse
python -m pytest tests/test_access_matrix.py affiche 213 passed (213 vérifications réussies). Une vérification ignorée n’a pas été exécutée, généralement parce que PostgreSQL n’est pas en cours d’exécution. Elle ne prouve rien : comptez seulement les vérifications réussies.
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
- Les tests négatifs vérifient ce qui doit échouer, et le refus exact : 401, 403 ou 404.
- Une matrice de tests envoie chaque personne, dans chaque organisation, vers chaque route : 7 × 2 × 11 = 154 cellules. Avec 4 cas utilisant l’identifiant de l’autre boutique et 5 mauvaises identités × 11 routes, cela donne 213 vérifications.
- Un bogue construit dans la liste des membres a réussi les 234 tests précédents et échoué à 2 vérifications de la matrice.
- La matrice fait confiance à
decide();test_access_policy.pyvérifiedecide()par rapport à la matrice papier. Gardez les deux. - Un test ignoré n’est pas un test réussi.
- Ensuite : consignez qui a fait quoi, et qui a essayé.
Leçon suivante : Auditabilité