Wie mag wat: elk endpoint apart
Een controle beschermt alleen het endpoint waar hij in staat. Heb je
opdracht 5 van het htmx-overzicht
gemaakt, dan heeft je eigen gastenboek een knop Alles wissen, met een eigen
endpoint DELETE /berichten. Daar staat geen controle in: wie dat endpoint
aanroept, wist elk bericht, ook dat van een ander. De controle in
bericht_verwijderen verandert daar niets aan. Een endpoint dat je later
toevoegt, heeft hetzelfde probleem als je de grens vergeet.
Loop daarom elk endpoint langs dat iets verandert of verwijdert, en stel bij elk dezelfde vraag: wie mag dit? Voor Alles wissen is het antwoord: iedereen, maar alleen voor zijn eigen berichten.
Alles wissen, maar alleen van jou
Zet dit endpoint onder de andere in main.py:
@app.delete("/berichten")
async def alles_wissen(sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
with SqliteDict("gastenboek.db") as db:
van_mij = []
for sleutel, bericht in db.items():
if bericht["naam"] == mijn.get("naam"):
van_mij.append(sleutel)
for sleutel in van_mij:
del db[sleutel]
db.commit()
return {"bericht": "Jouw berichten zijn gewist"}
- Regel 2-4:
De sessie van deze bezoeker, net als in
bericht_verwijderen. - Regel 6-9:
Eerst zoekt de functie de berichten met de naam uit de sessie, en zet hun sleutels in een lijst.
- Regel 10-11:
Daarna pas gaan ze weg. Bij een gewone dictionary mag je niets weghalen terwijl je er nog doorheen loopt; met de lijst ertussen hoeft dat ook niet.
Test het
Maak een script test_wissen.py, waarin Alex alles wist:
import httpx
adres = "http://127.0.0.1:8000"
sara = httpx.Client(base_url=adres)
alex = httpx.Client(base_url=adres)
sara.post("/registreer", data={"naam": "Sara", "wachtwoord": "zon123"})
sara.post("/inloggen", data={"naam": "Sara", "wachtwoord": "zon123"})
alex.post("/registreer", data={"naam": "Alex", "wachtwoord": "maan456"})
alex.post("/inloggen", data={"naam": "Alex", "wachtwoord": "maan456"})
sara.post("/bericht", data={"bericht": "Hoi allemaal"})
alex.post("/bericht", data={"bericht": "Leuk gastenboek"})
print(alex.delete("/berichten").json())
namen = []
for bericht in alex.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.
Hier is geen 403 nodig: de grens zit erin dat de functie alleen berichten
met de naam uit de sessie pakt.
Er gaat iets mis
Method Not Allowed
Het script print:
{'detail': 'Method Not Allowed'}
Nog in het gastenboek: ['Sara', 'Alex']
Oorzaak: je server heeft nog geen DELETE /berichten, wel een
GET /berichten. Het pad bestaat dus, maar niet met deze methode, en dan geeft
FastAPI 405.
Oplossing: zet alles_wissen in main.py en sla op. fastapi dev start de
server dan vanzelf opnieuw.
KeyError: 'naam'
Een verzoek zonder sessie, zoals het script in opdracht 1, stopt met
json.decoder.JSONDecodeError, zodra er berichten in het gastenboek staan. In de terminal van je server eindigt de
foutmelding met:
KeyError: 'naam'
Oorzaak: je schreef mijn["naam"], zoals in bericht_plaatsen. Daar
staat eerst de 401 als er geen sessie is; in alles_wissen niet. Zonder
sessie is mijn een lege dictionary, en die heeft geen sleutel naam. Bij
het eerste bericht in de for-lus antwoordt je server met een 500, en die
tekst is geen JSON.
Oplossing: schrijf mijn.get("naam"), zoals hierboven, of zet eerst de
401 erin, zoals in opdracht 1.
Opdrachten
Opdracht 1: Modify - Eerst inloggen
Een bezoeker zonder sessie krijgt nu Jouw berichten zijn gewist, terwijl er
niets weg is. Laat alles_wissen hem weigeren met 401 Log eerst in, net als
bericht_plaatsen. Test het met een verzoek zonder Client:
import httpx
antwoord = httpx.delete("http://127.0.0.1:8000/berichten")
print(antwoord.status_code, antwoord.json())
Tip
Kijk in bericht_plaatsen wat er direct na het ophalen van de sessie staat.
Antwoord
Zet na het ophalen van de sessie, vóór het with-blok van gastenboek.db:
if not mijn:
raise HTTPException(status_code=401, detail="Log eerst in")
Uitvoer:
401 {'detail': 'Log eerst in'}
Nu zegt het antwoord eerlijk dat er niets gewist is. De lessen hierna gaan verder met de versie zonder deze twee regels; je mag ze laten staan.
Opdracht 2: Predict - Allebei Alles wissen
Sara heeft drie berichten in het gastenboek, Alex één. Eerst roept Sara Alles wissen aan, daarna Alex.
Vraag: wat geeft /berichten daarna?
Antwoord
{}, een leeg gastenboek. Sara wist haar drie berichten, Alex daarna het zijne.
Geen van beiden kwam aan een bericht van de ander: samen wissen ze alles, maar
elk alleen het eigen deel.
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"}
@app.delete("/berichten")
async def alles_wissen(sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
with SqliteDict("gastenboek.db") as db:
van_mij = []
for sleutel, bericht in db.items():
if bericht["naam"] == mijn.get("naam"):
van_mij.append(sleutel)
for sleutel in van_mij:
del db[sleutel]
db.commit()
return {"bericht": "Jouw berichten zijn gewist"}
Elk endpoint dat iets verwijdert, controleert nu zelf.
Door naar les 6: in je eigen project.