Ga naar hoofdinhoud

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"}
  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, en daarin is mijn.get("naam") gelijk aan None.

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

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

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. · Een knop is geen slot

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

  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.

Controlevraag
Sara is ingelogd en stuurt een DELETE voor het bericht van Alex. Wat antwoordt de server nu?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

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.