Wie mag wat: de controle met 403
In les 2 verwijderde Alex het bericht van Sara, met of zonder knop. De controle hoort daarom op de plek waar het verzoek binnenkomt: in het endpoint. De server weet wie deze bezoeker is: sinds het inloggen staat zijn naam in zijn sessie. En bij elk bericht staat de naam van wie het plaatste. Zijn die twee niet gelijk, dan verwijdert het endpoint niets.
Heb je opdracht 5 van Sessies
gemaakt, dan deed je dit al, op /verwijderen. Hier zet je dezelfde controle op
DELETE /bericht/{sleutel}.
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, {})
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()
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, en daarin is
mijn.get("naam")gelijk aanNone. - Regel 7-9:
Eerst de controle, vóór er iets verandert. Bestaat het bericht niet, of staat er een andere naam bij dan in de sessie, 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 10-11:
Pas nu gaat het bericht weg.
Doe de test opnieuw
Zet onderaan test_verwijderen.py uit les 2 een stuk
waarin Sara haar eigen bericht verwijdert. Het is dezelfde lus als voor Alex,
met sara.get en sara.delete:
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 = []
for bericht in sara.get("/berichten").json().values():
namen.append(bericht["naam"])
print(f"Nog in het gastenboek: {namen}")
Gooi gastenboek.db, sessies.db en gebruikers.db weg, start de server
opnieuw, en draai het script.
Alex stuurt nog steeds hetzelfde verzoek, maar de server weigert. De Client
van Sara stuurt haar cookie mee, en in haar sessie staat de naam Sara, dezelfde
naam als bij het bericht.
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. · Een knop is geen slot
Server: Het endpoint haalt de sessie op. Uit de cookie het sessie-id, uit sessies.db de naam waarmee Alex inlogde. · Onthouden op de server: 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
NameError: name 'sessie_id' is not defined
Het script print Alex verwijdert het bericht van Sara: 500 en stopt daarna
met een foutmelding van httpx. 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.
Een 'Alex' te veel
Het script print bijvoorbeeld:
Alex verwijdert het bericht van Sara: 403
Nog in het gastenboek: ['Alex', 'Sara', 'Alex']
Sara verwijdert haar eigen bericht: 200
Nog in het gastenboek: ['Alex', 'Alex']
Oorzaak: je hebt de databases niet weggegooid. Het bericht van Alex uit
les 2 staat er nog, naast de
twee nieuwe; draaide je dat script vaker, dan staan er meer. De controle werkt
wel: Alex krijgt 403 en Sara 200.
Oplossing: gooi gastenboek.db, sessies.db en gebruikers.db weg, en
draai het script opnieuw.
Opdrachten
Opdracht 1: Predict - Een nieuw venster
Sara opent het gastenboek in een privévenster. Daar logt ze opnieuw in, als
Sara, en stuurt ze het verzoek om haar eigen bericht te verwijderen. In het
script is dat een derde httpx.Client, die met haar naam en wachtwoord
inlogt.
Vraag: krijgt ze 200 of 403?
Tip
Waarmee vergelijkt de controle de naam bij het bericht: met de browser, of met de naam in de sessie?
Antwoord
200. Het privévenster heeft een ander sessie-id, maar onder dat id staat ook
de naam Sara, want ze logde in met haar wachtwoord. De controle kijkt naar die
naam, niet naar het venster. Logt ze in het privévenster níét in, dan heeft ze
geen sessie en krijgt ze 403.
Opdracht 2: Investigate - Een sleutel die niet bestaat
Alex stuurt alex.delete("/bericht/bestaat_niet"). Welke statuscode krijgt hij,
en welk deel van de controle geeft die?
Antwoord
403, met {'detail': 'Dit is niet jouw bericht'}. db.get geeft None, en
dan is bericht is None waar. Eén voorwaarde vangt allebei de gevallen: een
bericht dat niet bestaat en een bericht van een ander. Zo staat er maar één
controle vóór de del, en kan een niet-bestaande sleutel nooit bij
bericht["naam"] een TypeError geven.
Een 404 zou hier niets verraden, want /berichten laat elke sleutel al zien.
Op een site waar de sleutels niet openbaar zijn, is het wel een reden om
dezelfde 403 te geven: dan leert een bezoeker niet welke sleutels bestaan,
net zoals Inloggen: een wachtwoord controleren
één melding geeft voor een onbekende naam en een fout wachtwoord.
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("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as gebruikers:
if naam in gebruikers:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
gebruikers[naam] = wachtwoord
gebruikers.commit()
return {"bericht": f"Welkom, {naam}"}
@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as gebruikers:
opgeslagen = gebruikers.get(naam)
if opgeslagen != wachtwoord:
raise HTTPException(status_code=401, detail="Naam of wachtwoord klopt niet")
sessie_id = secrets.token_hex(16)
with SqliteDict("sessies.db") as sessies:
sessies[sessie_id] = {"naam": naam}
sessies.commit()
antwoord = JSONResponse({"bericht": f"Ingelogd als {naam}"})
antwoord.set_cookie(key="sessie_id", value=sessie_id, max_age=60 * 60 * 24 * 30)
return antwoord
@app.post("/bericht")
async def bericht_plaatsen(bericht: str = Form(...), sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
if not mijn:
raise HTTPException(status_code=401, detail="Log eerst in")
sleutel = f"bericht_{time.time_ns()}"
with SqliteDict("gastenboek.db") as db:
db[sleutel] = {"naam": mijn["naam"], "bericht": bericht}
db.commit()
return {"sleutel": sleutel}
@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, {})
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()
return {"bericht": "Verwijderd"}
Het verwijderen controleert nu wie het vraagt.
Door naar les 4: controleer op wat de server weet.