From a46377e7bc3a9cb3256c5ebd08e0deff6c980120 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sat, 18 Jul 2026 16:19:52 +0200 Subject: [PATCH] Update Todos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Utvid team_authz.py: org owner/admin får overstyre uten unntak Oppdater alle 5 kallsteder i matches.py/scoring.py Scratch-verifisering: org-admin uten roster kan låse/legge til deltakere/score, vanlig member kan ikke, rostret spiller uendret Rulle ut live + oppdatere statusdokumenter Live, verifisert, og dokumentert. Klar for å skrive V0-prompten for blind draw-skjermen når du vil — samme mønster som de forrige, og nå med bekreftet organisator-tilgang selv om ingen kaptein har logget inn ennå. --- .claude/settings.local.json | 4 +++- CLAUDE.md | 23 +++++++++++++++++++ FEATURE_BACKLOG.md | 19 +++++++++++++-- app/routers/matches.py | 4 ++-- app/routers/scoring.py | 6 ++--- app/team_authz.py | 46 ++++++++++++++++++++++++++++++++----- 6 files changed, 88 insertions(+), 14 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 77730a0..89308cf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -248,7 +248,9 @@ "Bash(python3 -)", "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(len\\(d\\), 'facilities'\\); print\\(d[:2]\\)\")", "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/scratch_cookies2.txt)", - "Bash(curl -s \"https://teecup.teeoff.no/tournaments/73b6bf38-90cd-4ef7-9535-8b8e108eaa9a?org=test-org-id&name=Test\")" + "Bash(curl -s \"https://teecup.teeoff.no/tournaments/73b6bf38-90cd-4ef7-9535-8b8e108eaa9a?org=test-org-id&name=Test\")", + "Bash(python3 -m py_compile app/team_authz.py app/routers/matches.py app/routers/scoring.py)", + "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/owner_cookies.txt /tmp/member_cookies.txt /tmp/captain_cookies.txt)" ], "additionalDirectories": [ "/opt/teeoff/deploy", diff --git a/CLAUDE.md b/CLAUDE.md index c786176..4d4e829 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -776,6 +776,29 @@ Ferdig og verifisert: ALDRI være et eget `
` -- bruk `
` + eksplisitt klikk-/Enter-håndtering. +- **Organisator-overstyring i `team_authz.py`, LIVE (2026-07-18):** reist av + brukeren rett før blind draw-skjermen skulle designes: `app/team_authz.py` + sin `user_may_act_for_team` krevde tidligere en `team_roster`-rad med + lenket bruker-konto — ingen vei for organisatoren til å låse/legge til + deltakere/føre score hvis ingen spiller hadde logget inn ennå (vanlig + tidlig i en turnering). Utvidet til også å godta org-eier/admin + (`organization_membership.role IN ('owner','admin')`), på ETHVERT lag, + ingen unntak for at organisatoren selv er rostret på motstanderlaget. + **Det unntaket ble bevisst vurdert og avvist** (brukeren spurte selv om + det, jeg anbefalte det opprinnelig, men vi kom sammen frem til at det ville + skapt en verre låsning: er organisatoren spillende på Lag A og + Lag B heller ikke har noen innlogget spiller, ville Lag B blitt helt + låst ute) — TeeCup er et tillitsbasert klubb-/vennegjeng-verktøy, ikke en + sikkerhetsgrense mot en fiendtlig organisator som uansett allerede ser + begge lags fulle troppe-liste (blind draw skjuler kun selve + kamp-paringen). Alle 5 kallsteder oppdatert (matches.py sin + add_participant/lock_lineup, scoring.py sin submit_hole_score/ + submit_hole_result). **Verifisert grundig i scratch:** org-eier uten + roster kan nå låse BEGGE lag + føre score, en vanlig 'member'-rolle + fortsatt blokkert (uendret), en rostret spiller fungerer uendret + uavhengig av org-rolle. `test_isolation.sql` 12/12. Ingen migrasjon (ren + Python-endring) — kun `teecup_api` redeployet, `teeoff.no` upåvirket. + Neste steg: 1. Blind draw-skjermen (neste i "bygg i rekkefølgen ting brukes", gjenopptatt etter ADR-019-sidespranget) — deretter scorekort, leaderboard. Samme diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 3113a7b..2cf551b 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -309,11 +309,26 @@ bevisst IKKE «kun én kaptein per lag» — flere spillere kan i dag merkes kaptein samtidig på samme lag. Bør revurderes samtidig med resten av dette punktet, ikke løses isolert i roster-endepunktet. +- **Nytt 2026-07-18, ✅ BYGGET:** `app/team_authz.py` sin `user_may_act_for_team` + (brukt av lås/deltakere i matches.py OG score-skriving i scoring.py) gir nå + også org-eier/admin (`organization_membership.role IN ('owner','admin')`) + samme rettigheter som en rostret spiller, på ETHVERT lag — reist av + brukeren rett før blind draw-skjermen: uten dette kunne INGEN sette opp + eller låse et lag før minst én spiller hadde logget inn og blitt koblet. + Bevisst INGEN unntak for at organisatoren selv er rostret på + MOTSTANDERLAGET (vurdert og avvist — se team_authz.py sin docstring for + full begrunnelse: tillitsbasert verktøy, organisator ser uansett begge + rostre allerede, og et unntak ville skapt en reell låsning der ingen + kunne sette opp motstanderlaget om det heller ikke har en innlogget + spiller). Verifisert i scratch: org-eier uten roster kan nå låse begge + lag + føre score, vanlig 'member'-rolle fortsatt blokkert, rostret + spiller uendret. Rullet ut live samme dag. ### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16) - **Status:** ❓ trenger beslutning — direkte oppfølger av «Brukerroller» over. -- **Hvem fører score i dag:** alle med en `team_roster`-rad på laget (samme - minimale grense som deltaker/lås, se ADR-013-relatert kode). Ikke +- **Hvem fører score i dag:** alle med en `team_roster`-rad på laget, ELLER + org-eier/admin (utvidet 2026-07-18, se «Brukerroller» over) — samme + minimale grense som deltaker/lås, se ADR-013-relatert kode. Ikke kaptein-only, ikke begrenset til de(n) som faktisk spiller matchen. - **Hvem kan korrigere en ført score i dag:** akkurat de samme — skriving er en upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en diff --git a/app/routers/matches.py b/app/routers/matches.py index 614036f..e7e6aeb 100644 --- a/app/routers/matches.py +++ b/app/routers/matches.py @@ -267,7 +267,7 @@ async def add_participant( if tee_course_id is None or tee_course_id != session["course_id"]: raise app_error(400, "OUT_OF_SCOPE", "tee_id tilhører ikke øktens bane.") - if not await user_may_act_for_team(conn, expected_team_id, user.user_id): + if not await user_may_act_for_team(conn, organization_id, expected_team_id, user.user_id): raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.") locked = await locked_team_ids(conn, match["session_id"]) @@ -338,7 +338,7 @@ async def lock_lineup( if session is None: raise app_error(404, "NOT_FOUND", "Økten finnes ikke.") - if not await user_may_act_for_team(conn, body.team_id, user.user_id): + if not await user_may_act_for_team(conn, organization_id, body.team_id, user.user_id): raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.") # UNIQUE(session_id, team_id) gir 409 via translate_db_errors ved diff --git a/app/routers/scoring.py b/app/routers/scoring.py index 8ca1e97..8037501 100644 --- a/app/routers/scoring.py +++ b/app/routers/scoring.py @@ -272,7 +272,7 @@ async def submit_hole_score( 400, "MISMATCHED_SIDE", "match_participant_id tilhører ikke angitt side." ) - if not await user_may_act_for_team(conn, expected_team_id, user.user_id): + if not await user_may_act_for_team(conn, organization_id, expected_team_id, user.user_id): raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.") if body.match_participant_id is not None: @@ -365,8 +365,8 @@ async def submit_hole_result( # Begge lag kan rapportere et hull-resultat (hvilken som helst side kan # vinne/tape/dele) -- brukeren må være rostret på ETT av de to lagene. if not ( - await user_may_act_for_team(conn, match["team_a_id"], user.user_id) - or await user_may_act_for_team(conn, match["team_b_id"], user.user_id) + await user_may_act_for_team(conn, organization_id, match["team_a_id"], user.user_id) + or await user_may_act_for_team(conn, organization_id, match["team_b_id"], user.user_id) ): raise app_error( 403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på noen av lagene i denne matchen." diff --git a/app/team_authz.py b/app/team_authz.py index f8a1efe..8058336 100644 --- a/app/team_authz.py +++ b/app/team_authz.py @@ -1,16 +1,33 @@ """ -Delt autorisasjonssjekk: er brukeren rostret på et gitt lag? +Delt autorisasjonssjekk: kan brukeren handle på vegne av et gitt lag? -Brukt av både matches.py (deltaker/lås-skriving) og scoring.py (score-skriving) -— se plan for begrunnelsen (bevisst IKKE kapteins-only ennå, FEATURE_BACKLOG). +Brukt av både matches.py (deltaker/lås-skriving) og scoring.py (score-skriving). + +To uavhengige veier inn: +1. Rostret på laget -- spiller med `player.user_id` koblet på nettopp DENNE + team_roster-raden (den opprinnelige, fortsatt gjeldende regelen). +2. Org-eier/admin i organisasjonen laget tilhører. Lagt til 2026-07-18: uten + dette kunne INGEN sette opp eller låse et lag før minst én spiller hadde + logget inn og blitt koblet -- vanlig tidlig i en turnering, og + organisatoren (som uansett allerede ser begge lags fulle troppe-liste, + blind draw skjuler kun selve kamp-paringen) satt fast. + + Bevisst INGEN unntak for at organisatoren selv er rostret på + MOTSTANDERLAGET i samme turnering -- vurdert og avvist: TeeCup er et + tillitsbasert verktøy for klubber/vennegjenger, ikke en sikkerhets- + grense mot en fiendtlig organisator (som uansett har full administrativ + tilgang), og et slikt unntak ville skapt en reell låsning (ingen kan + sette opp MOTSTANDERLAGET om det heller ikke har noen innlogget spiller + -- verre enn problemet det skulle løse). """ from asyncpg import Connection -async def user_may_act_for_team(conn: Connection, team_id: str, user_id: str) -> bool: - """Brukeren har en team_roster-rad på laget.""" - return await conn.fetchval( +async def user_may_act_for_team( + conn: Connection, organization_id: str, team_id: str, user_id: str +) -> bool: + is_rostered = await conn.fetchval( """ SELECT EXISTS ( SELECT 1 FROM team_roster tr @@ -21,3 +38,20 @@ async def user_may_act_for_team(conn: Connection, team_id: str, user_id: str) -> team_id, user_id, ) + if is_rostered: + return True + + # 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. + is_org_admin = 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(is_org_admin)