Ga naar hoofdinhoud

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"}
  1. Regel 2:

    Het endpoint vraagt nu ook de cookie op, net als bericht_plaatsen.

  2. Regel 3-4:

    De sessie van deze bezoeker. Geen cookie of een onbekend sessie-id geeft een lege dictionary.

  3. 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.

  4. Regel 8-11:

    Pas nu gaat het bericht weg. if sleutel in db vangt 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.

Waar de controle zit: wie mag wat
  1. Browser: Alex stuurt een DELETE. Met de sleutel van het bericht van Sara; die staat gewoon in de pagina. · De zwakheid

  2. Server: Het endpoint haalt de sessie op. Uit de cookie het sessie-id, uit sessies.db de sleutels van Alex. · Sessies

  3. 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

  4. 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.