teecup/app/team_authz.py

244 lines
9.6 KiB
Python
Raw Normal View History

"""
Nå dokumentasjonen er oppdatert. Klar for deploy — jeg viser planen først: Update Todos Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen Avklare designbeslutninger for Brukerroller med bruker Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk) Oppdatere matches.py til å bruke user_is_team_captain Oppdatere scoring.py til å bruke user_is_match_participant Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing) Skrive ADR-023 for Brukerroller-beslutningene Sette opp scratch MinIO + engangs API-container Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et Rydde opp scratch-infrastruktur Ekte typesjekket frontend-build (øktkort-hint) Deploy etter eksplisitt bekreftelse + oppdatere .md-filer Alt er scratch-verifisert. Klar for utrulling — planen: Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt). docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere. Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg. Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
2026-07-19 11:41:57 +02:00
Delt autorisasjonssjekk: to distinkte spørsmål, to funksjoner.
1. `user_is_team_captain` -- "kan brukeren sette opp/fjerne/låse LAGETS
oppstilling?" Brukt av matches.py sin add_participant/remove_participant/
lock_lineup, FØR noen match_participant-rad i det hele tatt finnes for
brukeren selv (kylling-og-egg: man kan ikke kreve at brukeren ALLEREDE er
deltaker for å lov til å LEGGE TIL deltakere).
2026-07-19 (Brukerroller-runden): byttet fra "hvem som helst rostret på
laget" til kaptein (`team_roster.is_captain = true`) ELLER org-eier/admin
-- kapteinen er en reell autorisasjonsrolle, ikke bare et visningsmerke.
Se ARCHITECTURE_DECISIONS.md for hele begrunnelsen.
Bevisst unntak: har laget INGEN utpekt kaptein ennå, godtas enhver
rostret spiller i stedet for å låse laget helt ute -- vanlig tidlig i en
turnering før noen har rukket å utpeke en kaptein (og en reell risiko
funnet i eksisterende produksjonsdata: langt fra alle roster-rader har en
kaptein i dag). Har laget FØRST fått en kaptein (`update_roster_entry`
håndhever "kun én kaptein per lag"), er det utelukkende den som
gjelder -- ingen andre rostrede spillere.
2. `user_is_match_participant` -- "kan brukeren føre/korrigere score for
DENNE spesifikke matchen?" Brukt av scoring.py. Krever en faktisk
match_participant-rad for brukeren i akkurat denne matchen (valgfritt
begrenset til én side via `team_side`) -- IKKE bare rostret laget, og
UAVHENGIG av kapteinmerket (å være kaptein gir ikke i seg selv rett til å
føre score for en match man selv ikke spiller).
Begge har samme org-eier/admin-fallback, av samme grunn som tidligere
(2026-07-18-runden): uten den kunne INGEN sette opp eller føre noe før minst
én spiller hadde logget inn og blitt koblet til sin player-rad. Bevisst
INGEN unntak for at organisatoren selv er rostret MOTSTANDERLAGET i samme
turnering -- vurdert og avvist tidligere: TeeCup er et tillitsbasert verktøy
for klubber/vennegjenger, ikke en sikkerhetsgrense mot en fiendtlig
organisator (som uansett har full administrativ tilgang), og et slikt
unntak ville skapt en reell låsning.
"""
from asyncpg import Connection
async def is_org_admin(conn: Connection, organization_id: str, user_id: str) -> bool:
# organization_membership har ingen RLS-policy (se auth.get_authorized_org
# sin egen kommentar) -- filtrert eksplisitt på organization_id her, trygt
# på samme tilkobling uansett hvilken app.current_org som er satt.
result = await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM organization_membership
WHERE organization_id = $1 AND user_id = $2 AND role IN ('owner', 'admin')
)
""",
organization_id,
user_id,
)
return bool(result)
Nå dokumentasjonen er oppdatert. Klar for deploy — jeg viser planen først: Update Todos Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen Avklare designbeslutninger for Brukerroller med bruker Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk) Oppdatere matches.py til å bruke user_is_team_captain Oppdatere scoring.py til å bruke user_is_match_participant Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing) Skrive ADR-023 for Brukerroller-beslutningene Sette opp scratch MinIO + engangs API-container Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et Rydde opp scratch-infrastruktur Ekte typesjekket frontend-build (øktkort-hint) Deploy etter eksplisitt bekreftelse + oppdatere .md-filer Alt er scratch-verifisert. Klar for utrulling — planen: Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt). docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere. Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg. Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
2026-07-19 11:41:57 +02:00
async def user_is_team_captain(
conn: Connection, organization_id: str, team_id: str, user_id: str
) -> bool:
has_captain = await conn.fetchval(
"SELECT EXISTS (SELECT 1 FROM team_roster WHERE team_id = $1 AND is_captain = true)",
team_id,
)
if has_captain:
may_act = await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM team_roster tr
JOIN player p ON p.id = tr.player_id
WHERE tr.team_id = $1 AND p.user_id = $2 AND tr.is_captain = true
)
""",
team_id,
user_id,
)
else:
may_act = await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM team_roster tr
JOIN player p ON p.id = tr.player_id
WHERE tr.team_id = $1 AND p.user_id = $2
)
""",
team_id,
user_id,
)
if may_act:
return True
return await is_org_admin(conn, organization_id, user_id)
Nå dokumentasjonen er oppdatert. Klar for deploy — jeg viser planen først: Update Todos Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen Avklare designbeslutninger for Brukerroller med bruker Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk) Oppdatere matches.py til å bruke user_is_team_captain Oppdatere scoring.py til å bruke user_is_match_participant Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing) Skrive ADR-023 for Brukerroller-beslutningene Sette opp scratch MinIO + engangs API-container Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et Rydde opp scratch-infrastruktur Ekte typesjekket frontend-build (øktkort-hint) Deploy etter eksplisitt bekreftelse + oppdatere .md-filer Alt er scratch-verifisert. Klar for utrulling — planen: Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt). docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere. Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg. Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
2026-07-19 11:41:57 +02:00
async def user_is_match_participant(
conn: Connection,
organization_id: str,
match_id: str,
user_id: str,
team_side: str | None = None,
) -> bool:
is_participant = await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM match_participant mp
JOIN team_roster tr ON tr.id = mp.team_roster_id
JOIN player p ON p.id = tr.player_id
WHERE mp.match_id = $1 AND p.user_id = $2
AND ($3::text IS NULL OR mp.team_side::text = $3)
)
""",
match_id,
user_id,
team_side,
)
if is_participant:
return True
return await is_org_admin(conn, organization_id, user_id)
Update Todos Flytte is_participant-logikk til team_authz.py (unngå sirkulær import) Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup Widen tournaments.py: list_sessions/list_teams/concede_tournament Widen courses.py: list_holes Widen messaging.py: team chat REST-endepunkter (list/send/delete) Legge til my_session_id/my_match_id i /auth/me sin my_tournaments Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder» Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller) Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster) Bygge HCP-historikk over tid Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort: Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder". Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match). Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
async def is_org_member(conn: Connection, organization_id: str, user_id: str) -> bool:
"""ETHVERT medlemskap (ikke bare owner/admin, se is_org_admin over) --
brukt som den ene halvparten av OR-et som (2026-07-21, deltaker-tilgang-
runden) erstattet den tidligere blanke get_authorized_org-sperren
read-endepunkter som selv ikke har noen mer finkornet sjekk (list_teams,
list_sessions, get_scorecard) -- bevarer eksisterende org-medlemmers
tilgang uendret, samtidig som en ikke-medlem deltaker slipper gjennom
via user_is_tournament_participant under."""
return bool(
await conn.fetchval(
"SELECT EXISTS (SELECT 1 FROM organization_membership WHERE organization_id = $1 AND user_id = $2)",
organization_id,
user_id,
)
)
async def user_is_org_player(conn: Connection, organization_id: str, user_id: str) -> bool:
"""Er brukeren koblet til NOEN spillerprofil i denne organisasjonen (ikke
nødvendigvis en gitt turnering) -- brukt for lavsensitiv data som
bane-/hull-info (courses.py sin list_holes), der en full
turnering-deltaker-sjekk ville vært unødvendig presis for hva som
faktisk beskyttes (par/stroke-index, ikke spillerdata)."""
return bool(
await conn.fetchval(
"SELECT EXISTS (SELECT 1 FROM player WHERE organization_id = $1 AND user_id = $2)",
organization_id,
user_id,
)
)
async def user_is_tournament_participant(
conn: Connection, organization_id: str, tournament_id: str, user_id: str
) -> bool:
"""Har brukerens koblede spillerprofil (ADR-017 Beslutning B) en
registrering ELLER en rostret plass i NØYAKTIG denne turneringen?
Flyttet hit fra registration.py 2026-07-21 (het `is_participant` der) --
org-scopede endepunkter (matches.py/scoring.py/tournaments.py) kan ikke
importere fra registration.py uten sirkulær import (registration.py
importerer FRA disse tre for øvrig), men alle tre importerer allerede
fritt fra denne (dependency-frie) modulen. registration.py importerer
denne i stedet for sin egen kopi."""
return bool(
await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM player p
WHERE p.organization_id = $1 AND p.user_id = $2
AND (
EXISTS (
SELECT 1 FROM tournament_registration tr
WHERE tr.player_id = p.id AND tr.tournament_id = $3
)
OR EXISTS (
SELECT 1 FROM team_roster tro
JOIN team t ON t.id = tro.team_id
WHERE tro.player_id = p.id AND t.tournament_id = $3
)
)
)
""",
organization_id,
user_id,
tournament_id,
)
)
async def user_is_own_tournament_participant(
conn: Connection, organization_id: str, tournament_participant_id: str, user_id: str
) -> bool:
"""Individuelle turneringer (ADR-037): er brukeren SELV spilleren bak
denne `tournament_participant`-raden (via `player.user_id`)?
Mirror av `user_is_match_participant`, men uten `team_side` -- en
individuell turnering har ingen lag å begrense siden til. Brukt av
`individual_tournaments.py` sin `update_hole` (2026-07-30): før denne
kunne ETHVERT org-medlem skrive score for EN HVILKEN SOM HELST
deltaker, ikke bare sin egen -- samme klasse hull `user_is_match_
participant` lukket for lagturneringer i ADR-023. Kun scoring er
strammet inn her; runde-/deltaker-oppsett (opprett runde, legg til/
fjern turnering-/rundedeltaker) forblir bevisst org-medlemsnivå,
samme presedens som `session`-opprettelse og `team_roster`-tilføyelse
i tournaments.py -- ingen av dem er captain-/admin-gatet heller, kun
selve VALGET AV HVEM SOM SPILLER EN GITT MATCH/RUNDE og selve
scoreregistreringen er det."""
is_self = await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM tournament_participant tp
JOIN player p ON p.id = tp.player_id
WHERE tp.id = $1 AND p.user_id = $2
)
""",
tournament_participant_id,
user_id,
)
if is_self:
return True
return await is_org_admin(conn, organization_id, user_id)
async def user_is_rostered_on_team(conn: Connection, team_id: str, user_id: str) -> bool:
"""Kun for lag-chat (ADR-025 Beslutning C) -- BEVISST INGEN org-admin-
fallback, ulikt de to funksjonene over. «Det hemmelige rommet» er ekte
privat: kun spillere faktisk rostret laget, aldri organisatoren, uansett
org-rolle. Ikke gjenbruk denne for autorisasjon utenfor chat."""
return bool(
await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM team_roster tr
JOIN player p ON p.id = tr.player_id
WHERE tr.team_id = $1 AND p.user_id = $2
)
""",
team_id,
user_id,
)
)