Wie mag wat: in je eigen project
In deze reeks zag je dat een knop geen slot is, dat de controle in het endpoint hoort, dat hij moet kijken naar wat de server weet, en dat elk endpoint er een nodig heeft. Nu test je je eigen gastenboek op dezelfde manier als de server van deze reeks.
Werk in je eigen project, niet in de map veiligheid-toegang. Test daarna met
je eigen server op 127.0.0.1.
Alex in je eigen gastenboek
Je gastenboek verwijdert op één of twee manieren: met POST /verwijderen uit
Een bericht verwijderen: een verborgen veld,
en, als je het htmx-recept Verwijderen bouwde, met DELETE /bericht/{sleutel}.
Het script test ze allebei.
Start je eigen server en schrijf in je browser een bericht als Sara, of als een
ander account. Zoek in het tabblad Elementen bij dat bericht de link naar de
detailpagina, <a href="/bericht/bericht_…">, en kopieer het stuk vanaf
bericht_.
Maak in de map van je project een script test_eigen.py. Het maakt een account
voor Alex, logt in en probeert het bericht te verwijderen. Zet bij sleutel de
waarde die je kopieerde:
import httpx
alex = httpx.Client(base_url="http://127.0.0.1:8000")
gegevens = {"naam": "Alex", "wachtwoord": "maan4567"}
print("Registreren:", alex.post("/registreer", data=gegevens).status_code)
print("Inloggen:", alex.post("/inloggen", data=gegevens).status_code)
sleutel = "bericht_1767000000000000000"
print("Bestaat:", alex.get(f"/bericht/{sleutel}").status_code)
print("POST /verwijderen:", alex.post("/verwijderen", data={"sleutel": sleutel}).status_code)
print("DELETE /bericht:", alex.delete(f"/bericht/{sleutel}").status_code)
Registreren en inloggen geven 303: je gastenboek stuurt door, zoals na elk
formulier. Bestaat vraagt de detailpagina op; 200 betekent dat de sleutel
klopt. Draai het script.
Staat er bij POST /verwijderen of DELETE /bericht iets anders dan 403 of
405, dan staat dat endpoint open. Zet de controle erin zoals bij
Er gaat iets mis, schrijf een nieuw bericht als Sara en
draai het script opnieuw met de nieuwe sleutel.
Er gaat iets mis
Bestaat: 404
Het script print Bestaat: 404, en daarna bij POST /verwijderen een 303.
Oorzaak: de sleutel bestaat niet. Je liet de voorbeeldsleutel staan, of je
kopieerde er te weinig of te veel van, zoals /bericht/ ervoor. Je
POST /verwijderen vindt het bericht dan niet, verwijdert niets en stuurt je
door. De 303 zegt hier niets over de controle, en de regel van
DELETE /bericht ook niet.
Oplossing: kopieer de sleutel opnieuw, vanaf bericht_ tot het
aanhalingsteken, en draai het script nog een keer.
POST /verwijderen: 303
Het script print Bestaat: 200 en POST /verwijderen: 303, en het bericht van
Sara is weg.
Oorzaak: je POST /verwijderen heeft nog geen controle. Hij verwijdert elk
bericht waarvan hij de sleutel krijgt, en stuurt de bezoeker daarna door. De
regel van DELETE /bericht zegt dan niets meer, want het bericht is al weg.
Oplossing: zet sessie_id: str = Cookie(default="") naast sleutel, en
zet in de functie de controle vóór de del, zoals in
opdracht 5 van Sessies.
Binnen de functie staat dan:
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
with SqliteDict("gastenboek.db") as db:
if sleutel in db:
if db[sleutel]["naam"] != mijn.get("naam"):
raise HTTPException(status_code=403, detail="Dit is niet jouw bericht")
del db[sleutel]
db.commit()
De return RedirectResponse(...) eronder blijft.
DELETE /bericht: 200
Het script print POST /verwijderen: 403, maar DELETE /bericht: 200, en het
bericht van Sara is weg.
Oorzaak: de knop van het htmx-recept gebruikt DELETE /bericht/{sleutel},
en daar staat de controle nog niet in. Het formulier naar /verwijderen is
beschermd, maar dat gebruikt je pagina niet meer.
Oplossing: zet sessie_id: str = Cookie(default="") naast sleutel, en
zet de controle vóór de del. Binnen de functie staat dan:
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
with SqliteDict("gastenboek.db") as db:
bericht = db.get(sleutel)
if bericht is None or bericht["naam"] != mijn.get("naam"):
raise HTTPException(status_code=403, detail="Dit is niet jouw bericht")
del db[sleutel]
db.commit()
De return HTMLResponse("") eronder blijft. Bij een 403 laat htmx de pagina
met rust: het bericht blijft staan, en in Netwerk zie je een rode regel.
Registreren: 400 en Inloggen: 401
Het script print Registreren: 400 en Inloggen: 401. Dan zeggen de 403's
daarna niets: zonder sessie weigert de controle iedereen, ook de eigenaar.
Oorzaak: je /registreer weigerde Alex. Of Alex bestaat al in je
gebruikers.db met een ander wachtwoord, of je /registreer stelt eisen aan
het wachtwoord die maan4567 niet haalt. De melding zie je met
print(alex.post("/registreer", data=gegevens).text).
Oplossing: kies in gegevens een naam die nog niet bestaat, en een
wachtwoord dat aan je eigen eisen voldoet.
Opdracht
Opdracht 1: Make - In je eigen project
Loop in je eigen project elk endpoint langs dat iets verandert of verwijdert. Zorg dat elk ervan zelf controleert of de bezoeker het mag.
Tip
Zoek naar @app.post en @app.delete. Kan iedereen iets toevoegen, zoals een
bericht in een gastenboek, dan is dat een bewuste keuze. Maar verwijderen of
aanpassen hoort alleen bij de eigenaar. Kijk bij @app.get wat er terugkomt:
alleen wat elke bezoeker mag zien?
Antwoord
Loop deze lijst af:
- Elk endpoint dat verwijdert of aanpast, haalt de sessie op met
sessie_id: str = Cookie(default=""). - De controle staat vóór het verwijderen of aanpassen, en stuurt anders een
403. - Je controleert op wat de server zelf weet, zoals de naam in de sessie, niet op een naam uit het formulier, de URL of een cookie.
- Een verborgen knop is een hulp, geen slot: het endpoint controleert toch.
- Ook een
GETis een gat als hij gegevens van anderen teruggeeft. StaatGET /sessiesuit opdracht 3 van Sessies nog in je project, haal hem weg: hij geeft elk sessie-id weg, en met het sessie-id van Sara is Alex voor je server ingelogd als Sara, zonder haar wachtwoord. Elke403is dan omzeild.
Heb je in Onthouden op de server: sessies
opdracht 5 gemaakt, dan heeft POST /verwijderen deze controle al. Bouwde je
het htmx-recept Verwijderen, zet hem dan ook in DELETE /bericht/{sleutel},
zoals bij DELETE /bericht: 200 hierboven.
Heb je een knop Alles wissen, dan krijgt alles_wissen dezelfde grens als in
les 5. In je gastenboek
gaf die functie uit het htmx-overzicht de lijst-template terug, met een lege
lijst. Houd die TemplateResponse en de parameter request: Request, en zet
sessie_id: str = Cookie(default="") ernaast. Lees na het wissen, nog in het
with-blok, wat er over is met alle_berichten = list(db.items()), en geef
{"berichten": alle_berichten} mee in plaats van {"berichten": []}: de
berichten van anderen blijven staan.
Door naar les 7: in de praktijk.