0%

Wie wat mag, en hoe vaak · oefening

Elk boek heeft een eigenaar

Les 1 voorspelde dat ben het boek van ana kan verwijderen. Kijk hoe je server het doet. Voer in de uit:

python pl_sec.py other-reader

pl_sec.py start je server op 127.0.0.1 en stuurt hem zes verzoeken:

pl_sec: other-reader, your server on 127.0.0.1 (a port the system chose)
pl_sec sends the __Host-session cookie by hand; a browser would not send it over plain HTTP
--- ben gets a token of his own ---
ben logs in with a password         POST /login   -> 200
ben asks for a token, with the cookie  POST /tokens  -> 200
--- what ben can reach with it ---
ben lists the books             GET /books      -> 200  {"books": [{"id": 1, "title": "Kindred", "author": "Octavia E. Butler"}, {"id": 2, "title": "Café Society", "author": "Ana Ruiz"}]}
ben deletes book 1, ana's       DELETE /books/1 -> 204
ben reads book 1                GET /books/1    -> 404  {"error": "no book has that id"}
a request with no token         GET /books      -> 200  {"books": [{"id": 2, "title": "Café Society", "author": "Ana Ruiz"}]}
--- result ---
ben deleted ana's book: 204
serve() returned after 6 connections; the server is closed
pl_sec: every thread has stopped

Elk inloggegeven in die run is echt. ben logde in met zijn eigen wachtwoord en gebruikte zijn eigen token. Dit is bevinding F8.

Schrijf eerst de aanval

De test herhaalt het verzoek van ben. steps.keyed_state() geeft een voorbeeldstate met de testsleutel erin, en steps.bearer_for(state, "ben", "battery staple") logt ben in, vraagt POST /tokens om een token, en geeft de van Authorization terug.

Laat ben eerst een eigen boek toevoegen en controleer 201. Dat is de vooraf: de test gaat over een schrijver wiens token werkt. Stuur daarna DELETE /books/1 met hetzelfde token, en controleer dat de status 404 is en dat 1 nog in state["books"] staat.

Voer python -m pytest -q uit. Het eindigt met 1 failed, 17 passed, en de mislukking luidt assert 204 == 404.

Een eigenaar die de client niet kan kiezen

Een boek heeft een eigenaar nodig voordat een regel ernaar kan vragen. Drie plekken zetten die.

Geef in sample_state() beide boeken "owner": "ana".

create_book krijgt een derde , owner, en slaat die op in het nieuwe boek. route geeft claims["user"] mee. Die naam komt uit een token dat je server ondertekende, dus de client kan hem niet kiezen. Een body die "owner": "ana" zegt, verandert niets, omdat create_book nog steeds alleen title en author uit de body leest.

replace_book houdt de eigenaar die het opgeslagen boek al heeft. Hoofdstuk 7 gaat een stap verder en weigert een body die velden bevat waar niemand om vroeg.

Eén regel, twee vragen

may_view(user, book) beslist over elke lezing, en may_change(user, book) over elke schrijfactie. Vandaag zijn ze allebei dezelfde zin: de eigenaar van het boek is deze gebruiker. Het zijn twee omdat het twee vragen zijn. Op de dag dat een boek met een vriend gedeeld kan worden, verandert er maar één.

Beide geven False terug als user None is of book None is, voordat ze iets vergelijken. Dat sluit de valkuil None == None uit les 1.

Weigeren, tenzij een regel het toestaat

Herschrijf nu item_refusal. De meegeleverde versie somt op wat geweigerd moet worden en eindigt met de rest toestaan. Draai het om:

  • Zonder claims geef je unauthorized("Bearer") terug, voor elke .

  • GET en HEAD geven None terug als may_view het toestaat.

  • PUT en DELETE geven de 403 terug als het account helemaal niet mag schrijven, en None als may_change het toestaat.

  • Elke andere methode geeft not_allowed("GET, HEAD, PUT, DELETE") terug, maar alleen voor een boek dat de aanroeper mag bekijken.

  • De laatste regel geeft json_error(404, "no book has that id") terug.

return None staat nu alleen nog binnen een regel. Alles wat geen regel toestond, en elk geval waar niemand aan dacht, eindigt bij de laatste regel. Die vorm heet standaard weigeren (deny by default).

Er staan nu drie weigeringen naast elkaar. 403 is voor een account dat helemaal niet mag schrijven, zoals in with Python. 404 is voor een boek dat niet van de aanroeper is, of het nu bestaat of niet. 405 is alleen voor een boek dat de aanroeper mag bekijken, omdat een 405 voor het boek van een ander zou bevestigen dat de id bestaat.

Volgend jaar laat iemand PATCH bij item_refusal uitkomen en schrijft er geen regel voor. Wat krijgt een PATCH van ben op het boek van ana met de functie die jij schreef?

Lezen is nu persoonlijk

collection_refusal krijgt dezelfde eerste regel: geen claims, geen toegang, voor elke methode. /books lezen vraagt nu een token, en de over de houdt alleen de boeken die may_view toestaat. Dit is een bewuste verandering ten opzichte van HTTP with Python, waar de lijst voor iedereen open was.

Een antwoord dat afhangt van wie erom vroeg, mag niet uit een cache aan iemand anders worden geserveerd. Het project zet al Cache-Control: private, no-cache op beide boeklezingen. private betekent dat een gedeelde cache het antwoord niet mag opslaan. no-cache betekent wat het in HTTP with Python betekende: je mag het bewaren, maar vraag het mij voordat je het gebruikt.

Het tweede woord is nodig omdat de eigen cache van een browser een opgeslagen antwoord vindt op zijn , niet op het token. Met een max-age zou een lijst die voor de ene lezer is opgeslagen, een minuut lang getoond kunnen worden bij een verzoek dat het token van een andere lezer draagt. Met no-cache vraagt de browser opnieuw met If-None-Match, en de server, die het token ziet, beslist elke keer: 304 als de eigen lijst van de aanroeper niet veranderd is, anders 200 met de lijst van de aanroeper.

Of een cache gehoorzaamt, is de zaak van de cache. Het deel van je server is het zeggen. GET /about blijft openbaar en houdt max-age=60.

ben vraagt om /books/1, dat van ana is, en om /books/4100, dat niet bestaat. Waarom moeten de twee antwoorden gelijk zijn?

Wat vraagt Cache-Control: private, no-cache?

Je opdracht

  1. Schrijf test_attack_another_reader_cannot_delete_a_book() en zie hem falen.

  2. Geef beide voorbeeldboeken een eigenaar, geef create_book zijn parameter owner, en houd de opgeslagen eigenaar in replace_book.

  3. Schrijf may_view en may_change.

  4. Herschrijf collection_refusal en item_refusal zo dat None alleen onder een regel wordt teruggegeven.

  5. Houd in de lus over de lijst in route alleen de boeken die may_view(claims["user"], book) toestaat.

  6. Vul rij F8 van SECURITY.md, noem je test erin, en zet de status op closed in 6.2.

Voer python -m pytest -q opnieuw uit: 18 passed. Voer daarna python pl_sec.py other-reader nog een keer uit. De lijst van ben is leeg, zijn verwijdering en zijn lezing van boek 1 krijgen allebei 404, het verzoek zonder token krijgt 401, en de run eindigt met ben was refused: 404, and book 1 is still there.

Eén ding blijft zichtbaar. Boek-id’s tellen voor iedereen nog steeds omhoog, dus de id die een nieuw boek krijgt, laat zien hoeveel boeken er in totaal bestaan. En de login die ben zijn token gaf, mag nog steeds zo vaak geprobeerd worden als iemand wil. Dat zijn de volgende twee lessen.

Opdracht

Geef elk boek een eigenaar, en laat de toegangsbeslissing weigeren tenzij een regel het toestaat.

  1. Schrijf in tests/test_attacks.py test_attack_another_reader_cannot_delete_a_book(). Haal met state = steps.keyed_state() bearer = steps.bearer_for(state, "ben", "battery staple") op. Stuur POST /books met een geldig boek en het token van ben in de header Authorization, en controleer status 201. Stuur DELETE /books/1 met het token van ben. Controleer status 404 en dat 1 nog in state["books"] staat. Voer python -m pytest -q uit: deze test moet eerst falen.

  2. Geef in reading_server.py, in sample_state(), beide boeken "owner": "ana".

  3. Geef create_book een derde , owner, sla die op in het boek, en geef claims["user"] mee waar route hem aanroept. Houd in replace_book de eigenaar die het opgeslagen boek al heeft.

  4. Schrijf may_view(user, book) en may_change(user, book): False als user None is of book None is; anders of book.get("owner") gelijk is aan user.

  5. Herschrijf collection_refusal(method, claims): geen claims is unauthorized("Bearer") voor elke ; GET en HEAD zijn toegestaan; POST is toegestaan als claims["can_write"] en is anders een 403; al het andere is not_allowed("GET, HEAD, POST").

  6. Herschrijf item_refusal(method, claims, book) zo dat hij alleen None teruggeeft als een regel het toestaat: geen claims is 401; GET en HEAD hebben may_view nodig; PUT en DELETE antwoorden 403 als het account niet mag schrijven en hebben may_change nodig; elke andere methode op een boek dat de aanroeper mag bekijken, is not_allowed("GET, HEAD, PUT, DELETE"); de laatste regel geeft json_error(404, "no book has that id") terug.

  7. Houd in de over de in route alleen de boeken die may_view(claims["user"], book) toestaat. Laat de twee regels met PRIVATE_CACHE zoals ze zijn.

  8. Vul rij F8 van SECURITY.md, noem je test in de cel Test, en zet de status op closed in 6.2.

Voer python -m pytest -q uit tot het eindigt met 18 passed, en klik daarna op Check my work. Die roept may_view, may_change, collection_refusal en item_refusal aan met eigen gebruikers en boeken. Hij stuurt verzoeken via reading_server.handle(...) met tokens die hij zelf ondertekent: de lijst van een lezer en de Cache-Control ervan, het boek van een andere lezer met elke methode, een account dat alleen mag lezen, en body’s die een eigenaar noemen. Hij voert je test drie keer uit, tegen servers die hij op de plaats van reading_server.handle en reading_server.sample_state zet: een correcte, waar je test moet slagen; een waarvan de schrijfacties niet tegen de eigenaar gecontroleerd worden, waar hij moet falen; en een die elk verzoek weigert, waar hij ook moet falen. Hij leest rij F8 van SECURITY.md; de formulering van een cel wordt niet beoordeeld. Niets uit je wordt beoordeeld.