Wie mag wat: de controle met 403
De controle hoort op de plek waar het verzoek binnenkomt: in het endpoint. De server weet welke berichten deze bezoeker heeft geplaatst, want ze staan in zijn sessie. Staat de sleutel daar niet in, dan verwijdert het endpoint niets.
De controle in het endpoint
Vervang bericht_verwijderen in je main.py door deze:
@app.delete("/bericht/{sleutel}")
async def bericht_verwijderen(sleutel: str, sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
if sleutel not in mijn.get("berichten", []):
raise HTTPException(status_code=403, detail="Dit is niet jouw bericht")
with SqliteDict("gastenboek.db") as db:
if sleutel in db:
del db[sleutel]
db.commit()
return {"bericht": "Verwijderd"}
- Regel 2:
Het endpoint vraagt nu ook de cookie op, net als
bericht_plaatsen. - Regel 3-4:
De sessie van deze bezoeker. Geen cookie of een onbekend sessie-id geeft een lege dictionary.
- Regel 5-6:
Eerst de controle, vóór er iets verandert. Is de sleutel niet van deze bezoeker, dan stopt de functie met 403 Forbidden: de server begrijpt wat je vraagt, maar jij mag het niet. De andere codes staan op de startpagina.
- Regel 8-11:
Pas nu gaat het bericht weg.
if sleutel in dbvangt een bericht op dat al weg is, bijvoorbeeld omdat je het in een ander tabblad al had verwijderd.
Heb je in Sessies opdracht 5
gemaakt, dan herken je deze controle: het is dezelfde, nu op het
DELETE-endpoint.
Doe de test opnieuw
Gooi gastenboek.db en sessies.db weg, start de server opnieuw, en draai
test_verwijderen.py uit les 1:
Alex verwijdert het bericht van Sara: 403
Nog in het gastenboek: ['Sara', 'Alex']
Alex stuurt nog steeds hetzelfde verzoek, maar de server weigert. Het bericht van Sara staat er nog.
Waar de controle zit
De controle zit in het endpoint zelf, na het ophalen van de sessie en vóór het verwijderen.
Browser: Alex stuurt een DELETE. Met de sleutel van het bericht van Sara; die staat gewoon in de pagina. · De zwakheid
Server: Het endpoint haalt de sessie op. Uit de cookie het sessie-id, uit sessies.db de sleutels van Alex. · Sessies
Hier zit de controleServer: Is dit bericht van Alex?. Nee: 403, en er verandert niets. Pas bij ja gaat het bericht weg. · De controle met 403
Browser: Alex krijgt een 403. Het bericht van Sara staat er nog.
Er gaat iets mis
Het verwijderen geeft 500 Internal Server Error, ook bij je eigen bericht. In
de terminal van je server eindigt de foutmelding met:
NameError: name 'sessie_id' is not defined
Oorzaak: je hebt de nieuwe regels in bericht_verwijderen gezet, maar de
parameter sessie_id: str = Cookie(default="") niet. Dan bestaat sessie_id
niet in de functie.
Oplossing: zet de parameter erbij, zoals in regel 2 hierboven.
Opdrachten
Opdracht 1: Run - Sara mag wel
Zet onderaan test_verwijderen.py een stuk erbij waarin Sara haar eigen bericht
verwijdert. Lukt dat, en wat staat er daarna nog in het gastenboek?
Tip
Het is dezelfde lus als voor Alex, maar dan met sara.get en sara.delete.
Antwoord
for sleutel, bericht in sara.get("/berichten").json().items():
if bericht["naam"] == "Sara":
antwoord = sara.delete(f"/bericht/{sleutel}")
print(f"Sara verwijdert haar eigen bericht: {antwoord.status_code}")
namen = [bericht["naam"] for bericht in sara.get("/berichten").json().values()]
print(f"Nog in het gastenboek: {namen}")
Uitvoer, na de twee regels van Alex:
Sara verwijdert haar eigen bericht: 200
Nog in het gastenboek: ['Alex']
De Client van Sara stuurt haar cookie mee, en de sleutel staat in haar sessie.
Opdracht 2: Predict - Een nieuw venster
Sara opent het gastenboek in een privévenster en klikt daar op Verwijderen bij
haar eigen bericht. In het script is dat een derde httpx.Client, zonder
cookie.
Vraag: krijgt ze 200 of 403?
Antwoord
403. De server herkent een bezoeker aan zijn sessie-id, en een privévenster
heeft een ander, of nog geen. Voor de server is dat een nieuwe bezoeker, ook al
typt die "Sara". Wil je dat iemand overal bij zijn eigen berichten kan, dan
heb je een account nodig met een wachtwoord: zie de praktijk.
Opdracht 3: Investigate - Een sleutel die niet bestaat
Alex stuurt alex.delete("/bericht/bestaat_niet"). Welke statuscode krijgt hij,
en waarom is het goed dat het geen 404 is?
Antwoord
403, met {'detail': 'Dit is niet jouw bericht'}: de sleutel staat niet in
zijn sessie, en de controle komt vóór de vraag of het bericht bestaat. Zo leert
Alex niets over sleutels van anderen: een bestaand bericht van Sara en een
verzonnen sleutel geven hetzelfde antwoord. Hetzelfde idee kom je later tegen
bij inloggen, in de reeks over wachtwoorden.
Zo ziet je main.py er nu uit
import secrets
import time
from fastapi import Cookie, FastAPI, Form, HTTPException
from fastapi.responses import JSONResponse
from sqlitedict import SqliteDict
app = FastAPI()
@app.post("/bericht")
async def bericht_plaatsen(
naam: str = Form(...),
bericht: str = Form(...),
sessie_id: str = Cookie(default=""),
):
if not sessie_id:
sessie_id = secrets.token_hex(16)
sleutel = f"bericht_{time.time_ns()}"
with SqliteDict("gastenboek.db") as db:
db[sleutel] = {"naam": naam, "bericht": bericht}
db.commit()
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {"naam": naam, "berichten": []})
mijn["naam"] = naam
mijn["berichten"].append(sleutel)
sessies[sessie_id] = mijn
sessies.commit()
antwoord = JSONResponse({"sleutel": sleutel})
antwoord.set_cookie(key="sessie_id", value=sessie_id, max_age=60 * 60 * 24 * 30)
return antwoord
@app.get("/berichten")
async def berichten():
with SqliteDict("gastenboek.db") as db:
return dict(db)
@app.delete("/bericht/{sleutel}")
async def bericht_verwijderen(sleutel: str, sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
if sleutel not in mijn.get("berichten", []):
raise HTTPException(status_code=403, detail="Dit is niet jouw bericht")
with SqliteDict("gastenboek.db") as db:
if sleutel in db:
del db[sleutel]
db.commit()
return {"bericht": "Verwijderd"}
Het verwijderen controleert nu wie het vraagt.
Door naar les 3: controleer op wat de server weet.