L'autorisation dans l'API
- Leçon 11
- 55 min
- Module 4 : Contrôler l'accès aux ressources
Leçon 11 sur 19 · Authentification, autorisations et applications multilocatairesL'autorisation dans l'API
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
Faites répondre l’API aux trois questions pour chaque requête de billet de soutien. Qui appelle, dans quelle organisation, et que peut faire cette personne dans cette organisation? Un enregistrement d’une autre organisation ressemble alors exactement à un enregistrement qui n’existe pas.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les trois questions et la matrice d’accès, dans Trois questions distinctes et Utilisateurs et organisations. Les cinq tentatives dans Exemples de menaces.
- Les jetons d’accès, les portées et
/v1/me, dans Ce que signifie un jeton. Les sessions du navigateur, dans Choix de session dans le navigateur.
Votre projet
~/projects/ticket-apià l’instantanéend-of-m03:python -m pytestse termine par193 passed, 1 deselected.- Le dépôt des exercices à côté du projet, dans
~/projects/nextia-learning-labs. Exécutez-ygit pullpour obtenir l’instantanéend-of-m04. - Trois terminaux, chacun avec l’environnement virtuel actif : le fournisseur d’exercice (
python -m idp), l’API et un pour vos commandes.
Cette leçon copie les fichiers du module entier. Après cette étape, votre projet correspond à l’instantané end-of-m04. Les deux prochaines leçons expliquent les parties de cette même copie liées à la base de données et aux fichiers.
Un immeuble de bureaux avec plusieurs entreprises
Section intitulée « Un immeuble de bureaux avec plusieurs entreprises »Imaginez un immeuble de bureaux avec dix entreprises sur dix étages. À l’entrée, la réceptionniste vérifie votre carte d’identité. Cela répond à une question : qui vous êtes.
Ensuite, l’ascenseur vous mène seulement à l’étage de votre entreprise. Votre carte ne donne pas accès aux autres étages. Dans votre entreprise, une armoire porte l’inscription « Gestionnaires seulement », et un verrou la garde fermée. Votre carte l’ouvre seulement si vous y êtes gestionnaire.
Une réceptionniste attentive protège aussi les renseignements. Un visiteur demande : « Une entreprise appelée Acme se trouve-t-elle au 7e étage? » Si le visiteur n’y travaille pas, la réceptionniste répond : « Je ne peux pas vous aider avec cette question. » La réponse reste la même, qu’Acme existe ou non. Personne ne peut donc découvrir qui loue les locaux en posant des questions.
L’API de billets est maintenant cet immeuble. Larkfield et Bramble Books sont deux entreprises sur deux étages. Dans cette leçon, l’API reçoit son ascenseur et ses armoires verrouillées.
Le problème : le jeton connaît Sam, mais pas les billets
Section intitulée « Le problème : le jeton connaît Sam, mais pas les billets »Après le module 3, l’API peut identifier la personne qui appelle. Démarrez le fournisseur et l’API, puis demandez qui est Sam. Sous Windows, tapez curl.exe, et définissez le jeton avec $T = python -m scripts.login --user usr-sam --print-access-token.
T=$(python -m scripts.login --user usr-sam --print-access-token)curl -H "Authorization: Bearer $T" http://127.0.0.1:8000/v1/me{"user_id":"usr-sam","kind":"user","name":"Sam","platform_role":null,"client_id":"ticket-cli", "scopes":["members:manage","tickets:read","tickets:write"],"token_expires_at":"2026-10-10T06:38:37Z", "memberships":[{"tenant_id":"larkfield","tenant_name":"Larkfield","role":"staff"}]}La réponse est complète : Sam, membre du personnel, chez Larkfield. Envoyez maintenant le même jeton à une route de billets :
curl -H "Authorization: Bearer $T" http://127.0.0.1:8000/v1/tickets/T-40001{"error":{"code":"unauthorized","message":"A valid X-API-Key header is required.","request_id":"290c816753c5","fields":[]}}Les routes de billets demandent encore la clé partagée, et rien d’autre. Avec cette clé, Sam obtient le billet de Bramble, comme dans Exemples de menaces. Les 5 tentatives sur 5 ont obtenu ce qu’elles n’auraient pas dû obtenir. L’API sait qui est Sam, mais elle n’utilise pas cette information là où elle compte.
L’idée : l’organisation est dans le chemin, et le serveur la vérifie
Section intitulée « L’idée : l’organisation est dans le chemin, et le serveur la vérifie »Kwame écrit la règle au tableau : « Chaque requête nomme son organisation. Le serveur vérifie chaque fois que la personne qui appelle en fait partie, avant de lire quoi que ce soit. »
Toutes les routes des enregistrements d’une organisation passent donc sous le nom de cette organisation :
/v1/tickets/T-30002 before: no organization/v1/tenants/larkfield/tickets/T-30002 now: the organization is in the pathLe chemin indique quelle organisation la personne veut utiliser. L’API décide ensuite si cette personne peut l’utiliser. Elle prend cette décision à partir de sa propre table d’adhésions, jamais à partir de la requête. Cette décision est l’autorisation : ce qu’une personne identifiée peut faire. Le serveur la prend, car c’est la seule partie que la personne qui appelle ne peut pas modifier.
L’API vérifie quatre points, dans un ordre fixe, et s’arrête au premier « non » :
- Qui appelle? Un jeton ou une session vérifiés, comme dans les modules 2 et 3. S’il n’y en a pas : 401.
- Cette personne est-elle membre de l’organisation dans le chemin? Une ligne dans
memberships. S’il n’y en a pas : 404. - Le rôle et les portées du jeton permettent-ils l’action? La fonction
decide()de la matrice d’accès. Sinon : 403. - L’enregistrement appartient-il à cette organisation? La requête SQL contient
tenant_id = 'larkfield'. Si aucune ligne ne correspond : 404.
Description textuelle du diagramme
Une requête nomme son organisation dans le chemin et contient un jeton. La vérification 1 demande qui appelle; sans identité vérifiée, la réponse est 401. La vérification 2 cherche une ligne d’adhésion de cette personne dans cette organisation. Sans cette ligne, la réponse est 404 « Not found. » (introuvable), identique pour une organisation réelle ou inventée. La vérification 3 demande à la matrice d’accès si le rôle et les portées du jeton permettent l’action. Sinon, la réponse est 403. La vérification 4 est la requête SQL elle-même, qui contient l’organisation. Elle ne trouve pas un enregistrement d’une autre organisation : 404. Seule une requête qui passe les quatre vérifications obtient 200.
Pourquoi la vérification 2 précède-t-elle la vérification 3? Une personne externe n’a aucun rôle. Si l’API disait « 403 : votre rôle ne le permet pas », elle dirait aussi « cette organisation existe, et vous n’en faites pas partie ». Un 404 ne révèle rien.
Un petit exemple : une décision, quatre entrées
Section intitulée « Un petit exemple : une décision, quatre entrées »La vérification 3 est la fonction pure decide() du module 1. Dans le module 1, vous lui avez donné deux entrées : un rôle et une action. L’API lui en donne une troisième : les portées du jeton. Essayez-la en Python, dans le dossier du projet :
python -c "from ticket_api.access import decide; print(decide('read_only', 'ticket.write', frozenset({'tickets:read', 'tickets:write'})))"Decision(allowed=False, status=403, code='role_cannot', rule='read_only may not ticket.write')Modifiez une entrée à la fois pour obtenir chaque réponse de la matrice :
| Rôle dans l’organisation | Action | Portées du jeton | Réponse | Règle |
|---|---|---|---|---|
read_only |
ticket.write |
lecture et écriture | 403 | read_only ne permet pas ticket.write |
staff |
ticket.write |
lecture et écriture | 200 | staff permet ticket.write |
| aucun | ticket.read |
lecture et écriture | 404 | aucune adhésion à cette organisation |
staff |
ticket.write |
tickets:read seulement |
403 | le jeton n’a pas la portée tickets:write |
La dernière ligne rappelle un point du module 2. Un rôle indique ce que la personne peut faire. Une portée indique ce que la personne a permis à cette application de faire pour elle. Les deux doivent permettre l’action. Un script de production de rapports qui demande seulement tickets:read ne peut pas écrire, même pour un propriétaire.
Construisez-le
Section intitulée « Construisez-le »Tous les fichiers de ce module viennent de l’instantané end-of-m04. La copie inclut aussi les fichiers des deux prochaines leçons.
-
Arrêtez l’API avec Ctrl+C. Laissez le fournisseur fonctionner.
-
Dans
~/projects/ticket-api, copiez les fichiers du module depuis l’instantané :Fenêtre de terminal S=../nextia-learning-labs/C23/snapshots/end-of-m04for d in contract migrations scripts tests ticket_api; do cp -R "$S/$d/." "$d/"; donecp "$S/.env.example" .Fenêtre de terminal $S = "..\nextia-learning-labs\C23\snapshots\end-of-m04"foreach ($d in "contract","migrations","scripts","tests","ticket_api") { Copy-Item -Recurse -Force "$S\$d\*" $d }Copy-Item -Force "$S\.env.example" .Votre
.envne change pas. Les deux nouveaux paramètres,DB_ROW_SECURITYetCACHE_SECONDS, ont des valeurs par défaut. -
Appliquez les deux nouvelles migrations. Vos données restent, donc vous n’avez pas besoin de
--reset:Fenêtre de terminal python -m ticket_api.migrateapplied 016_tenant_indexesapplied 017_row_level_securityDatabase is up to date (17 migrations). -
Exécutez les tests :
Fenêtre de terminal python -m pytestLa dernière ligne est
218 passed, 1 deselected. Les tests du cours Bases de données et stockage pour les applications d’IA utilisent maintenant les chemins des organisations. -
Redémarrez l’API. La clé d’API protège encore
/v1/classifyet/v1/history:Fenêtre de terminal API_KEY=local-dev-key fastapi devSous Windows, définissez d’abord
$env:API_KEY = "local-dev-key", puis exécutezfastapi dev.
Lisez la vérification
Section intitulée « Lisez la vérification »Ouvrez ticket_api/tenancy.py. La fonction require(action) crée une dépendance FastAPI : une fonction que FastAPI exécute avant la route. Son cœur est court :
def check(request, db, identity, tenant_id, action) -> Caller: with db.connection() as conn: role = repository.membership_role(conn, tenant_id, identity.user_id) decision = decide(role, action, identity.scopes) if not decision.allowed: raise AccessDenied(decision, tenant_id, action) ...identity vient du jeton ou du témoin (cookie) de session, donc la vérification 1 a déjà eu lieu. membership_role() lit une ligne de memberships pour cette organisation et cet utilisateur. Ensuite, decide() répond.
Ouvrez maintenant ticket_api/tickets.py. Chaque route demande un Caller, et le préfixe du routeur contient l’organisation :
router = APIRouter(prefix="/v1/tenants/{tenant}/tickets", tags=["tickets"])Read = Annotated[Caller, Depends(require("ticket.read"))]
@router.get("/{ticket_id}", ...)def get_ticket(ticket_id: TicketId, caller: Read, db: DB) -> TicketDetail: with db.connection(caller.tenant_id) as conn: ticket = repository.get_ticket(conn, ticket_id, tenant_id=caller.tenant_id)Deux détails comptent. D’abord, la route a besoin d’un Caller pour s’exécuter, et seule la vérification peut en créer un. Ensuite, l’organisation de la requête SQL vient de caller.tenant_id : l’organisation que la vérification a acceptée. Elle ne vient jamais d’un en-tête ou du corps.
Les anciennes routes /v1/tickets… et /v1/attachments… ont disparu. Une clé partagée ne peut pas identifier la personne qui appelle, donc elle ne peut pas passer la vérification 2. La clé reste seulement pour /v1/classify et /v1/history, qui ne contiennent aucun enregistrement d’organisation. Ce sont des routes entre services, comme dans le cours Créer et déployer votre première API.
Essayez : quatre personnes, quatre réponses
Section intitulée « Essayez : quatre personnes, quatre réponses »Obtenez un jeton pour chaque personne. Chaque jeton dure 10 minutes. Sous Windows, utilisez $S = python -m scripts.login --user usr-sam --print-access-token, et ainsi de suite.
S=$(python -m scripts.login --user usr-sam --print-access-token)O=$(python -m scripts.login --user usr-omar --print-access-token)TO=$(python -m scripts.login --user usr-tomas --print-access-token)-
Sam lit un billet de sa propre organisation :
Fenêtre de terminal curl -i -H "Authorization: Bearer $S" http://127.0.0.1:8000/v1/tenants/larkfield/tickets/T-30002HTTP/1.1 200 OKcontent-type: application/jsoncache-control: no-store{"ticket_id":"T-30002","customer_id":"C-0022","customer_name":"Hugo Garcia","subject":"Charged twice for order LK-182074", …}Certains en-têtes et la majeure partie du corps sont omis ici.
-
Sam devine l’identifiant
T-40001de Bramble en passant par sa propre organisation :Fenêtre de terminal curl -H "Authorization: Bearer $S" http://127.0.0.1:8000/v1/tenants/larkfield/tickets/T-40001{"error":{"code":"not_found","message":"Ticket T-40001 does not exist.","request_id":"d084d5ebc331","fields":[]}}Dans Larkfield,
T-40001n’existe pas. Demandez aussiT-49999: le message est le même, avec l’autre identifiant. -
Sam essaie le chemin de Bramble :
Fenêtre de terminal curl -H "Authorization: Bearer $S" http://127.0.0.1:8000/v1/tenants/bramble/tickets/T-40001{"error":{"code":"not_found","message":"Not found.","request_id":"8f8d37f43163","fields":[]}}Sam n’est pas membre de Bramble Books. La vérification 2 l’arrête avant toute requête SQL de billet.
-
Omar, dont le rôle permet seulement la lecture, essaie d’écrire. Envoyez le même corps que dans le module 1 :
Fenêtre de terminal curl -X POST -H "Authorization: Bearer $O" -H "Content-Type: application/json" -d '{"author": "agent", "body": "Refund approved."}' http://127.0.0.1:8000/v1/tenants/larkfield/tickets/T-30002/messages{"error":{"code":"forbidden","message":"Your role may not do this (read_only may not ticket.write).","request_id":"6d55bd54c0d5","fields":[]}}Dans PowerShell, placez le corps dans
msg.jsonet utilisez-d "@msg.json". Le statut est 403, et l’API n’enregistre rien. -
Envoyez une requête sans jeton :
Fenêtre de terminal curl -i http://127.0.0.1:8000/v1/tenants/larkfield/tickets/T-30002HTTP/1.1 401 Unauthorizedwww-authenticate: Bearer realm="ticket-api"{"error":{"code":"not_signed_in","message":"Send an access token: Authorization: Bearer <token>.","request_id":"927221212ae2","fields":[]}} -
Créez un jeton avec
tickets:readseulement. L’utilitaire le signe avec les clés du fournisseur d’exercice, comme pour un script de production de rapports :Fenêtre de terminal R=$(python -c "from scripts.practice import token; print(token('usr-sam', 'tickets:read'))")curl -i -X POST -H "Authorization: Bearer $R" -H "Content-Type: application/json" -d '{"author": "agent", "body": "We refunded one payment."}' http://127.0.0.1:8000/v1/tenants/larkfield/tickets/T-30002/messagesHTTP/1.1 403 Forbiddenwww-authenticate: Bearer realm="ticket-api", error="insufficient_scope"{"error":{"code":"insufficient_scope","message":"The token has no tickets:write scope.","request_id":"17c4a6e6318e","fields":[]}}Le rôle de Sam permet l’écriture. Le jeton ne la permet pas.
Le terminal de l’API affiche chaque refus avec sa raison. La raison reste dans le journal, pas dans la réponse :
INFO ticket_api: access denied: role_cannot usr-omar ticket.write in larkfield id=6d55bd54c0d5INFO ticket_api: POST /v1/tenants/larkfield/tickets/T-30002/messages 403 2ms queries=1 id=6d55bd54c0d5Pourquoi 404 pour les personnes externes, et 403 pour les membres
Section intitulée « Pourquoi 404 pour les personnes externes, et 403 pour les membres »Tomás n’a aucune adhésion. Il demande Larkfield, une organisation réelle, puis acme, qui n’existe pas :
curl -H "Authorization: Bearer $TO" http://127.0.0.1:8000/v1/tenants/larkfield/tickets/T-30002curl -H "Authorization: Bearer $TO" http://127.0.0.1:8000/v1/tenants/acme/tickets/T-30002{"error":{"code":"not_found","message":"Not found.","request_id":"d2f7009169b7","fields":[]}}{"error":{"code":"not_found","message":"Not found.","request_id":"c4539997ef8d","fields":[]}}Seul le request_id diffère. Sous macOS et Linux, retirez-le et comparez les octets :
curl -s -H "Authorization: Bearer $TO" http://127.0.0.1:8000/v1/tenants/larkfield/tickets/T-30002 | sed 's/"request_id":"[0-9a-f]*",//' > real.txtcurl -s -H "Authorization: Bearer $TO" http://127.0.0.1:8000/v1/tenants/acme/tickets/T-30002 | sed 's/"request_id":"[0-9a-f]*",//' > made-up.txtcmp real.txt made-up.txt && echo identicalidenticalLe test test_an_outsider_gets_the_same_404_as_for_an_organization_that_does_not_exist fait la même comparaison à chaque exécution.
Description textuelle du diagramme
Les deux requêtes de Tomás visent une organisation réelle et une organisation inventée. Elles obtiennent le même corps de réponse 404, sauf l’identifiant de requête. Sam envoie deux requêtes par Larkfield : l’une avec un identifiant réel de Bramble, l’autre avec un identifiant absent. Les deux obtiennent le même message : « Ticket … does not exist. » (le billet … n’existe pas). Aucune des deux personnes n’apprend donc qu’un enregistrement ou une organisation existe. Les temps de réponse étaient identiques compte tenu du bruit : 2,03 ms et 2,02 ms dans les mesures du cours.
Une personne externe ne peut donc pas utiliser l’API pour lister les organisations ou vérifier quels identifiants de billets sont réels. Le cas d’un membre est différent. Omar est membre de Larkfield, et il peut déjà lire T-30002. Un 403 ne lui apprend rien de nouveau et lui donne une information utile : « votre rôle ne permet pas cela ». Les membres reçoivent donc 403, et les personnes externes reçoivent 404.
Il reste une limite à reconnaître. Le cours a mesuré le temps de chaque réponse : 200 requêtes, cinq passages, sur un ordinateur avec peu d’autres tâches actives. Le 404 de Tomás pour l’organisation réelle a pris une durée médiane de 2,03 ms. Pour l’organisation inventée, cette durée était de 2,02 ms. Ici, la différence se perd dans le bruit. Mais une route peut travailler davantage pour un enregistrement réel que pour un enregistrement absent, et répondre plus lentement. Une personne qui attaque peut répéter patiemment une requête de nombreuses fois et comparer les temps. C’est une fuite d’information temporelle. Certains systèmes ajoutent des délais aléatoires ou font le même travail dans tous les cas. Ce cours ne le tente pas. Il élimine la fuite dans le contenu de la réponse et précise que le temps peut encore révéler de l’information.
Exécutez les tentatives de nouveau
Section intitulée « Exécutez les tentatives de nouveau »Le script d’Exemples de menaces comporte maintenant dix tentatives. Chaque personne envoie son propre jeton, créé avec les clés du fournisseur d’exercice.
python -m scripts.attempts 2>/dev/nullMade-up attempts against your own practice API. Each person sends their own token.
1. Sam (staff at Larkfield) tries to read Bramble Books' ticket T-40001 through Larkfield's path. GET /v1/tenants/larkfield/tickets/T-40001 -> 404 (should get: 404) OK: not_found: Ticket T-40001 does not exist.2. Sam (staff at Larkfield) tries to read T-40001 through Bramble Books' path. GET /v1/tenants/bramble/tickets/T-40001 -> 404 (should get: 404) OK: not_found: Not found.3. Ines (owner at Bramble Books) tries to list the tickets of her own shop. GET /v1/tenants/bramble/tickets?limit=100 -> 200 (should get: 200) OK: 40 tickets, all bramble's4. Omar (read-only at Larkfield) tries to add a message to T-30002 (the page hides the button; he sends the request himself). POST /v1/tenants/larkfield/tickets/T-30002/messages -> 403 (should get: 403) OK: forbidden: Your role may not do this (read_only may not ticket.write).5. Tomás (no membership) tries to read Larkfield's ticket T-30002. GET /v1/tenants/larkfield/tickets/T-30002 -> 404 (should get: 404) OK: not_found: Not found.6. Ines (owner at Bramble Books) tries to find tickets similar to her ticket T-40001. GET /v1/tenants/bramble/tickets/T-40001/similar?k=5 -> 200 (should get: 200) OK: 5 tickets, all bramble's7. Sam (staff at Larkfield) tries to choose Bramble Books with a header (X-Tenant-ID: bramble) on Larkfield's list. GET /v1/tenants/larkfield/tickets?limit=100 -> 200 (should get: 200) OK: 100 tickets, all larkfield's8. Camille (read-only at Bramble Books) tries to add a message to Bramble Books' T-40001. POST /v1/tenants/bramble/tickets/T-40001/messages -> 403 (should get: 403) OK: forbidden: Your role may not do this (read_only may not ticket.write).9. Sam (staff at Larkfield) tries to get a download link for a Bramble Books receipt, with its real ID. GET /v1/tenants/larkfield/attachments/e7e54964-6167-47fa-8c59-b0b14b2d7e4a/download -> 404 (should get: 404) OK: not_found: Attachment e7e54964-6167-47fa-8c59-b0b14b2d7e4a does not exist.10. Omar (read-only at Larkfield) tries to search the documents for the staff-only refund procedure. GET /v1/tenants/larkfield/documents?q=refund approval -> 200 (should get: 200) OK: 2 public documents, no staff-only one
0 of 10 attempts got what they should not get.Dans PowerShell, utilisez 2>$null. Les tentatives 1 à 5 sont celles du module 1, qui avaient toutes réussi. Les tentatives 6, 9 et 10 dépendent aussi des deux prochaines leçons. Elles concernent le filtre de recherche, la recherche de fichiers et le niveau d’accès aux documents.
Quand l’utiliser, et ce que cela coûte
Section intitulée « Quand l’utiliser, et ce que cela coûte »Utilisez une organisation dans le chemin et une vérification sur le serveur pour chaque route qui touche les enregistrements d’une organisation. C’est le choix par défaut pour toute application partagée par plusieurs organisations. Une autre conception courante prend l’organisation dans le nom d’hôte, par exemple bramble.example.com. La règle reste la même : la requête nomme l’organisation, et le serveur vérifie l’adhésion.
Le coût est faible, mais réel. Chaque requête lit maintenant d’abord une ligne d’adhésion : le journal affiche queries=1 pour la requête refusée d’Omar. Toutes les routes et tous les tests ont changé de chemin, et le contrat de l’API a une nouvelle version. Certaines équipes placent les adhésions dans le jeton pour éviter cette requête SQL. Ce cours ne le fait pas, car un membre retiré garderait son accès jusqu’à l’expiration du jeton. Changements de cycle de vie mesure exactement cela.
Erreurs courantes
Section intitulée « Erreurs courantes »Erreur courante
Tomás : « Envoyez l'identifiant du locataire depuis le navigateur »
Symptôme : la page envoie X-Tenant-ID: bramble, et l’API s’en sert pour filtrer les résultats. Chaque démonstration fonctionne, car chaque utilisateur choisit sa propre organisation.
Cause : l’en-tête indique ce que l’appelant veut. N’importe quel appelant peut le modifier avec une seule option de curl. Essayez : Sam envoie cet en-tête pour la liste de Larkfield.
curl -H "Authorization: Bearer $S" -H "X-Tenant-ID: bramble" "http://127.0.0.1:8000/v1/tenants/larkfield/tickets?limit=3"Les trois éléments sont T-30200, T-30199 et T-30198 : les billets de soutien de Larkfield. L’API ignore l’en-tête.
Correction : prenez l’organisation uniquement à l’endroit que le serveur vérifie : le chemin, comparé à l’adhésion de l’appelant. ticket_api/tenancy.py ne lit jamais une organisation dans un en-tête, un paramètre de requête ou le corps. Le test test_a_tenant_header_from_the_browser_changes_nothing garantit que cela ne change pas.
Erreur courante
Tomás : « On a caché le bouton de suppression »
Symptôme : la page à /app/ affiche maintenant les billets de votre organisation. Connectez-vous en tant qu’Omar : la liste n’a pas de bouton « Reply » (répondre). Tomás pense que cela impose la règle de lecture seule.
Cause : la page se trouve dans le navigateur, et la personne contrôle le navigateur. À l’étape 4 ci-dessus, Omar a envoyé la requête sans utiliser la page.
Correction : gardez la règle dans require("ticket.write") sur le serveur. Cachez aussi le bouton pour faciliter l’utilisation : la page l’explique sous la liste. La requête d’Omar reçoit 403 dans les deux cas.
Erreur courante
Chaque requête reçoit 401 après quelques minutes
Symptôme : les commandes qui fonctionnaient reçoivent maintenant 401 avec "code":"invalid_token","message":"The token has expired." (jeton non valide, le jeton a expiré).
Cause : les jetons d’accès du fournisseur durent 10 minutes, comme dans Ce que signifie un jeton.
Correction : obtenez de nouveaux jetons avec les lignes python -m scripts.login ci-dessus.
Erreur courante
L'API répond 500 après la copie
Symptôme : chaque requête de billet reçoit 500 internal_error (erreur interne). Le terminal de l’API affiche psycopg.errors.InvalidParameterValue: role "ticket_app" does not exist (le rôle « ticket_app » n’existe pas).
Cause : le code est nouveau, mais la migration 017 n’a pas été exécutée. L’API passe au rôle ticket_app, que la migration crée. La prochaine leçon explique pourquoi.
Correction : exécutez python -m ticket_api.migrate, puis envoyez de nouveau la requête.
Réinitialisation. python -m scripts.load --reset charge de nouveau les deux boutiques et les personnes. Si votre projet ne fonctionne plus, copiez l’instantané end-of-m04 avec votre .env. Exécutez ensuite python -m scripts.load --reset et python -m idp init.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Expliquez pourquoi l'API répond 404, et non 403, quand Tomás demande un billet de Larkfield. Pourquoi 403 convient-il à l'écriture d'Omar?
Afficher la réponse
Tomás n’est pas membre de Larkfield. Un code 403 lui indiquerait que l’organisation existe. Un code 404 donne la même réponse que pour une organisation inventée, donc il n’apprend rien. Omar est un membre qui peut déjà lire le billet, donc un code 403 ne révèle rien de nouveau. Il lui indique aussi la vraie raison : son rôle ne permet pas d’écrire.
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 : Camille fait partie du personnel de Larkfield et a un accès en lecture seule à Bramble Books. Quelle réponse reçoit chaque requête? (a) GET /v1/tenants/bramble/tickets/T-40001; (b) POST d'un message à /v1/tenants/bramble/tickets/T-40001/messages; (c) GET /v1/tenants/larkfield/tickets/T-40001; (d) toute requête sans jeton.
Afficher la réponse
(a) 200 : Camille est membre, et le rôle read_only permet de lire. (b) 403 forbidden (interdit) : read_only ne permet pas ticket.write (tentative 8). (c) 404 « Ticket T-40001 does not exist. » (le billet T-40001 n’existe pas) : Camille passe les vérifications 1 à 3 chez Larkfield, mais la requête dans Larkfield ne trouve pas ce billet. (d) 401 not_signed_in (non connecté) : la vérification 1 arrête toute requête sans identité.
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 : montrez que les deux réponses 404 à une personne externe sont identiques octet par octet, sauf request_id. Quelles commandes exécutez-vous, et qu'affichent-elles?
Afficher la réponse
Obtenez le jeton de Tomás, demandez /v1/tenants/larkfield/tickets/T-30002 et /v1/tenants/acme/tickets/T-30002, puis retirez request_id avec sed. cmp real.txt made-up.txt && echo identical affiche identical (identique). Les deux corps sont {"error":{"code":"not_found","message":"Not found.","fields":[]}} (introuvable).
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
- Toutes les routes des enregistrements d’une organisation se trouvent sous
/v1/tenants/{tenant}/. Les anciennes routes ont disparu; la clé d’API reste seulement pour/v1/classifyet/v1/history. - Quatre vérifications, dans l’ordre : identité (401), adhésion (404), rôle et portée (403), enregistrement dans cette organisation (404).
- Une personne externe reçoit le même code 404 pour une organisation réelle ou inventée. Un membre reçoit 403 quand son rôle ou sa portée n’accorde pas assez d’autorisations. Le temps de réponse peut encore révéler de l’information; le cours n’a mesuré aucune différence ici (2,03 et 2,02 ms).
- L’organisation vient du chemin et de l’adhésion de l’appelant, jamais d’un en-tête. Un bouton caché n’est pas une règle.
python -m scripts.attempts: 0 tentative sur 10 a obtenu ce qu’elle ne devait pas obtenir.
Ensuite, vous sécurisez la partie base de données : l’organisation devient un argument obligatoire de chaque requête. La sécurité au niveau des lignes ajoute un deuxième verrou.
Leçon suivante : Requêtes sûres pour chaque locataire