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
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 Authorization terug.
Laat ben eerst een eigen boek toevoegen en controleer 201. Dat is de 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
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. GETenHEADgevenNoneterug alsmay_viewhet toestaat.PUTenDELETEgeven de 403 terug als het account helemaal niet mag schrijven, enNonealsmay_changehet 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
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 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 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
Schrijf
test_attack_another_reader_cannot_delete_a_book()en zie hem falen.Geef beide voorbeeldboeken een eigenaar, geef
create_bookzijn parameterowner, en houd de opgeslagen eigenaar inreplace_book.Schrijf
may_viewenmay_change.Herschrijf
collection_refusalenitem_refusalzo datNonealleen onder een regel wordt teruggegeven.Houd in de lus over de lijst in
routealleen de boeken diemay_view(claims["user"], book)toestaat.Vul rij F8 van
SECURITY.md, noem je test erin, en zet de status opclosed 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.
Schrijf in
tests/test_attacks.pytest_attack_another_reader_cannot_delete_a_book(). Haal metstate = steps.keyed_state()bearer = steps.bearer_for(state, "ben", "battery staple")op. StuurPOST /booksmet een geldig boek en het token van ben in de headerAuthorization, en controleer status 201. StuurDELETE /books/1met het token van ben. Controleer status 404 en dat1nog instate["books"]staat. Voerpython -m pytest -quit: deze test moet eerst falen.Geef in
reading_server.py, insample_state(), beide boeken"owner": "ana".Geef
create_bookeen derde, owner, sla die op in het boek, en geefclaims["user"]mee waarroutehem aanroept. Houd inreplace_bookde eigenaar die het opgeslagen boek al heeft.Schrijf
may_view(user, book)enmay_change(user, book):FalsealsuserNoneis ofbookNoneis; anders ofbook.get("owner")gelijk is aanuser.Herschrijf
collection_refusal(method, claims): geen claims isunauthorized("Bearer")voor elke; GETenHEADzijn toegestaan;POSTis toegestaan alsclaims["can_write"]en is anders een 403; al het andere isnot_allowed("GET, HEAD, POST").Herschrijf
item_refusal(method, claims, book)zo dat hij alleenNoneteruggeeft als een regel het toestaat: geen claims is 401;GETenHEADhebbenmay_viewnodig;PUTenDELETEantwoorden 403 als het account niet mag schrijven en hebbenmay_changenodig; elke andere methode op een boek dat de aanroeper mag bekijken, isnot_allowed("GET, HEAD, PUT, DELETE"); de laatste regel geeftjson_error(404, "no book has that id")terug.Houd in de
over de in routealleen de boeken diemay_view(claims["user"], book)toestaat. Laat de twee regels metPRIVATE_CACHEzoals ze zijn.Vul rij F8 van
SECURITY.md, noem je test in de cel Test, en zet de status opclosed 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