Skip to content

feat(web): EYT-113 costs.read-Ladegrenze an der ausgewaehlten Organisation - #99

Merged
DYAI2025 merged 11 commits into
masterfrom
feat/eyt-113-selected-org-cost-boundary
Aug 29, 2026
Merged

feat(web): EYT-113 costs.read-Ladegrenze an der ausgewaehlten Organisation#99
DYAI2025 merged 11 commits into
masterfrom
feat/eyt-113-selected-org-cost-boundary

Conversation

@DYAI2025

@DYAI2025 DYAI2025 commented Aug 29, 2026

Copy link
Copy Markdown
Owner

EYT-113 Inkrement 2 — costs.read-Ladegrenze an der ausgewählten Organisation

Schließt den offenen EYT-113-Akzeptanzpunkt: Kosten-Navigation und Kostenkomponenten werden nur nach verifiziertem costs.read geladen; serverseitige Autorisierung bleibt verbindlich — für /kosten und /kosten/stundensaetze, einschließlich Mehr-Organisation-Benutzern.

PO-Entscheidung umgesetzt: Kostenberechtigung bleibt organisationsbezogen; die Abschwächung „costs.read in irgendeiner Organisation" ist verboten und per ausgeführter Gegenmutation als rot bewiesen. Die ausgewählte Organisations-Id ist ausschließlich Selector, nie Autorisierungsquelle.

Basis und Umfang

  • Base: 028ccfb39f54db52d74b15b88434357371d39265 (= origin/master bei Brancherstellung und beim Push, frisch nachgemessen)
  • Branch: feat/eyt-113-selected-org-cost-boundary, Head: c174f127beb0937a899bd88206e5455ab6aa2470
  • 24 Dateien, +1890/−46, 10 Commits (3ad9fdac174f12), TDD-Historie: rote Beweise committet vor der Implementierung (d3ff4e1)

Implementierungsentscheidung

  1. Selector-Cookie eyt_org (lib/organisations-auswahl-cookie.ts): bewusst sichtbar (nicht HttpOnly, kein Geheimnis, kein Token; Session-Lebensdauer, SameSite=Lax). Geschrieben an genau einer Stelle (Kompositionswurzel app/providers.tsx im bestehenden onOrganisationChange-Seam, mit router.refresh() bei Wertänderung); beim Logout gelöscht (per Journey-Zusicherung bewiesen). Der Server verifiziert die Id gegen die real geprüfte Session (GET /auth/session): fremde/ungültige Id, fehlende Auswahl, fehlendes Recht, unbekannte Session ⇒ fail closed (lib/kosten-freigabe.ts, pure Matrix mit 9 Fällen).
  2. Server-Gate in beiden Pages: leseKostenFreigabe() als erstes Statement; Verweigerung rendert die Server-Flächen in components/kosten-grenze.tsx (kosten-forbidden / kosten-unauthenticated / kosten-org-auswahl / neu kosten-sitzung-unbekannt); headers()/cookies() machen beide Routen dynamisch (vorher war /kosten/stundensaetze statisch vorgerendert — bewusste Änderung; der Build braucht weiterhin kein EASYTREE_API_PROXY_TARGET, mit env -u … pnpm build --force belegt).
  3. Ladegrenze in drei gemessenen Stufen (D4-Messkette, nicht behauptet):
    • Stufe 1 (bedingtes Rendern bei statischen Imports): widerlegt — Member lud /_next/static/chunks/16y54lugq9hok.js trotz Server-Verweigerung.
    • Stufe 2 (server-seitiges await import() im gewährten Zweig): widerlegt — Next 16.2.11/Turbopack schreibt die komplette Client-Reference-Chunkliste der Route als unbedingte <script async>-Tags in den Denial-HTML-Head (direkt am ausgelieferten HTML gemessen); ein server-seitiger Dynamic-Import verschiebt das Modul-Laden, nicht den Manifest-Eintrag.
    • Stufe 3 (umgesetzt): Client-seitige Importgrenze. Die Routen referenzieren nur noch kostenfreie Loader (components/kosten-flaeche.tsx, components/stundensaetze-flaeche.tsx, next/dynamic); KostenZugang/KostenAnsicht/RateManagement sind Lazy-Chunks außerhalb der Route-Manifeste und laden erst, wenn der Server die Freigabe erteilt hat.
  4. Session-Helper geteilt statt dupliziert: lib/feld/sitzung-server.tslib/sitzung-server.ts (reiner Move, Rename-Diff = exakt die eine interne Importzeile, 97 % Similarity; drei Importeure nachgezogen).
  5. Auswahl-Semantik (Teil der Selector-Entscheidung, dokumentiert): die Auswahl überlebt jetzt einen Reload (Cookie-Restore, validiert über orgs.find gegen die Session); SessionProvider meldet die Auswahl nur noch in den Zuständen angemeldet/abgemeldet nach außen (ein transienter laedt/fehler-Nullwert löschte sonst das Cookie).

Organisations-/Permission-Matrix (jede Zeile real gemessen)

Zustand Verhalten Messanker
Eine Org mit costs.read Kosten erreichbar, Chunks laden Journey Haupttest Schritt 3/9g + Positivkontrolle 3b (angefordert non-leer)
Eine Org ohne costs.read (member) kosten-forbidden serverseitig, 0 Kosten-Chunk-Requests auf beiden Routen, Chunk-Dateinamen nicht im Dokument, keine Beträge Journey Test 3 (zusammenfassung-feld.json: ergebnis: passed)
Mehr-Org, keine Auswahl kosten-org-auswahl, 0 Chunk-Requests Mehr-Org-Test Phase a
Mehr-Org, gewählte Org ohne Recht (andere mit Recht) kosten-forbidden, 0 Chunk-Requests — das Recht der anderen Org überschreibt nichts Phase b; Serverlog decision:deny, reason:PERMISSION_MISSING (Phase d, 403 — der richtige Hebel, nicht ORG_NOT_A_MEMBER)
Direkte URL /kosten/stundensaetze im selben Zustand identisch verweigert Phase c
Mehr-Org, Wechsel auf Org mit Recht Inhalt + Lazy-Chunks laden (router.refresh-Pfad) Phase e
Reload nach Auswahl Auswahl überlebt (Cookie-Restore) Phase f
Cookie-Manipulation (fremde Id per context.addCookies) fail closed kosten-org-auswahl, keine neuen Chunk-Requests Phase g
Abgemeldet kosten-unauthenticated (Banner, kein neuer Redirect-Fluss) Unit-Matrix; Journey Schritt 12 (401)
Session unbekannt (API nicht erreichbar) kosten-sitzung-unbekannt, ausdrücklich ≠ abgemeldet, kein Kosten-Chunk shell-smoke (25/25, neuer Test 294 ms) + Unit
API unverändert bindend: 401/403-Zusicherungen der bestehenden Reise bleiben, Policy nicht angefasst Journey Schritte 1, 9g2, Phase d

Load-/RSC-/Chunk-Evidenz (Formen laut Auftrag)

  • Client-Reference-Manifeste: page_client-reference-manifest.js beider Kosten-Routen enthalten 0 Treffer für kosten-ansicht|kosten-zugang|rate-management (nur die Loader) — vorher beide Routen voll referenziert.
  • Playwright Network: Member/verweigerte Zustände [] Kosten-Chunk-Requests (Marker-abgeleitete Chunk-Menge zur Laufzeit aus .next/static/chunks, Helper wirft bei leerer Menge); Owner/gewährt ["0rp90oxbbgao1.js","13psdmgzzclg2.js"].
  • HTML/Dokument: Chunk-Dateinamen nicht in page.content() der verweigerten Routen; Stufe-2-Widerlegung direkt am Denial-HTML (<script src=".../3liqjy_g2e8wn.js" async> im Head) gemessen.
  • Route-/Build-Manifest: Route-Tabelle vorher ƒ /kosten, ○ /kosten/stundensaetze → nachher beide ƒ; prerender-manifest.json ohne Kosten-Routen.
  • Eine öffentlich abrufbare Chunk-URL wird nicht als Geheimnis geführt; maßgeblich ist, dass die verweigerte Journey sie weder anfordert noch referenziert.

TDD: vorher roter Test

  • Unit kosten-page-gate.test.tsx: 18 failed | 2 passed vor der Implementierung (Pages riefen leseKostenFreigabe 0-mal; Denial-Testids fehlten; Client-Wächter montierte). Committet als d3ff4e1, grün ab 8170bbb (20/20).
  • Journey: Ladegrenzen-Zusicherungen vor der Implementierung rot — Ist-Wert: Member forderte 16y54lugq9hok.js auf /kosten an (zugleich die Reproduktionsmessung des fehlenden Gates).

Gegenmutationen (alle ausgeführt: rot → restauriert → grün; Rücknahme je mit leerem, eingegrenztem git diff belegt)

  • M1 Gate-Bypass (stundensaetze/page.tsx, der geforderte Gegenbeweis): Architekturregel rot am echten Baum (Kanarie der neuen Regel kosten-server-gate), Unit 9 rot, Journey rot (/kosten/stundensaetze: angeforderte Kosten-Chunks erwartete [], erhielt ["0rp90oxbbgao1.js"]) — nach Restore Journey 5 passed.
  • M2 Any-Org-Rückfall (kosten-freigabe.ts): Fall 8 rot (fremder Selector fiele sonst auf die berechtigte Org zurück).
  • M3 unbekannt→abgemeldet (kosten-freigabe.ts): kosten-freigabe.test.ts rot. Befund: die Page-Gate-Suite blieb dabei grün (sie mockt das Freigabemodul) — ihr Kommentar behauptete das Gegenteil und wurde in c174f12 an die Messung angepasst (Mutationsebene KostenGrenze vs. Lib jetzt korrekt benannt).
  • M4 Effekt-Guard entfernt (session-provider.tsx): Fall (a) rot (Cookie-Löschung während laedt).
  • Architekturregel kosten-server-gate (9b46515): über den aufgelösten Import-Graphen (kein reiner Stringvergleich), Nicht-Leerlauf „≥ 2 Kosten-Pages gesehen" als eigene Violation, Rot-Beweis gegen synthetischen Baum in os.tmpdir() und Kanarie am echten Baum (M1).

Bewusste Änderung einer bestehenden Zusicherung

Journey-Schritt 11 („kein Token in Browserspeicher") prüfte pauschal document.cookie === "" — mit einem client-geschriebenen Selector-Cookie per Konstruktion unvereinbar. Geschärft statt abgeschwächt (5f88862): sichtbare Cookie-Namensmenge ⊆ {eyt_org} (jeder neue sichtbare Cookie macht den Schritt rot — strenger als vorher), Wert exakt die Fixture-Org-UUID, expliziter Token-Form-Wächter (kein .), localStorage/sessionStorage/DOM-Prüfungen unverändert; zusätzlich neu: Logout löscht eyt_org (beweist den Clear-Zweig).

Validierung (Evidenzlevel: lokal ausgeführt; CI auf diesem Head folgt als Required-Checks-Lauf)

  • turbo run lint|typecheck|test --force: je Cached: 0 cached, 6/10/10 Tasks; Tests: config 36, contracts 201, ui 91, domain 246, web 449 (38 Dateien), api 821 (+95 skipped = exakt die 13 mode=local-Integrationsgates bei gestopptem Stack — deren Pflichtbeweis ist der CI-Job db-gates)
  • env -u EASYTREE_API_PROXY_TARGET pnpm build --force: exit 0, Cached: 0 cached
  • Prettier über den getrackten Satz: „All matched files use Prettier code style!"
  • Playwright auth-journey (echte API dist/main.js + GoTrue + Supabase): 5 passed, mehrfach reproduziert (auch nach M1-Restore)
  • Playwright shell-smoke (ohne API, Laufzeit-Proxyziel gesetzt): 25 passed, bestehende /kosten-Smokes (Render/axe/Tokens) grün auf der neuen Unknown-Fläche
  • Fixture-Leihgabe eyt113-zweitorg-an/aus.sql: Prämissenwächter (A genau 1 Mitgliedschaft; ('member','costs.read') nicht in role_permissions; keine Restbestände) + Nachbedingungen, Marker [eyt113-zweitorg-an] leihe=1 a_aktiv=2 a_zweitorg_costs_read=f a_reiseorg_costs_read=t / [eyt113-zweitorg-aus] leihe=0 org=0 a_aktiv=1 owner_aktiv=1; Teardown restzeilen=0
  • Blocker-Grep über den Branch-Diff: 3 Substring-Treffer auf das deutsche Wort „offen" (Prosa in SQL-Kommentar, Bezeichner offeneSitzung), kein TODO/FIXME/TOOL_GAP

Screenshot-Artefakte (Playwright-Testartefakte, test-results/auth-journey/artefakte/…)

  • eyt113-kosten-positiv.png (Owner, gewährte Kostenfläche)
  • eyt113-kosten-forbidden.png (Member, Server-Forbidden)
  • eyt113-mehrorg-forbidden.png (Mehr-Org, gewählte Org ohne Recht)

Nicht enthalten / unverändert

Keine Migration, keine neuen API-Endpunkte, keine Änderung an MembershipCostAccessPolicy oder RLS, kein NEXT_PUBLIC_*, kein Eingriff in EYT-80/81/147/148. Rollback: git revert der Branch-Commits stellt den Vorzustand vollständig her; Feld-Verhalten per Ein-Zeilen-Rename-Diff + grünen Feld-Tests unberührt.

🤖 Generated with Claude Code

Summary by Sourcery

Enforce selected-organisation cost authorization and prevent unauthorized cost content from being loaded on either cost route.

New Features:

  • Enforce organisation-specific costs.read authorization for both cost routes using a verified organisation selector, with fail-closed handling for missing, invalid, or unknown sessions.
  • Prevent unauthorized users from loading or receiving cost client chunks until server-side access is granted.
  • Persist the selected organisation across refreshes and clear it on logout.

Bug Fixes:

  • Prevent permissions in another organisation or manipulated selector cookies from granting access to cost data.
  • Distinguish unknown sessions from logged-out users in cost access handling.

Enhancements:

  • Add architecture enforcement requiring every cost page to use the server access gate, including non-empty route coverage checks.
  • Centralize the server session helper and strengthen organisation-selection state handling.

Tests:

  • Add unit, architecture, shell smoke, and authenticated journey coverage for access decisions, selector-cookie behavior, chunk loading boundaries, multi-organisation scenarios, and API authorization.

BenPerro and others added 10 commits August 29, 2026 22:18
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ehlten Organisation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Loeschung bewiesen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ach Freigabe

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Gate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ie Messung angepasst

Die Suite mockt das Freigabemodul als Ganzes; die ausgefuehrte Lib-Mutation
M3 (unbekannt wie abgemeldet) liess sie messbar gruen. Die Kommentare nennen
jetzt die Mutationsebene, die diese Suite wirklich rot macht (KostenGrenze),
und verweisen fuer die Lib-Ebene auf kosten-freigabe.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @DYAI2025, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 4 days and 9 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Die Kostenoberflächen werden nun vor serverseitiger und clientseitiger Kostenladung organisationsbezogen geschützt: eyt_org wählt eine Organisation aus, die Freigabe wird ausschließlich gegen die verifizierte Session geprüft, verweigerte Routen rendern keine Kosten-Client-Referenzen oder -Chunks, und umfangreiche Unit-, Architektur-, Browser- und API-Nachweise decken Mehr-Organisations-, Manipulations- und Sitzungsfehlerfälle ab.

Sequence diagram for the organisation-scoped costs access flow

sequenceDiagram
    participant User
    participant SessionProvider
    participant Providers
    participant ServerPage
    participant KostenFreigabe
    participant KostenFlaeche
    participant KostenAPI

    User->>SessionProvider: selectOption
    SessionProvider->>Providers: onOrganisationChange
    Providers->>Providers: schreibeOrgAuswahl
    Providers->>ServerPage: router.refresh
    ServerPage->>KostenFreigabe: leseKostenFreigabe
    KostenFreigabe->>KostenFreigabe: leseServerSitzung
    KostenFreigabe->>KostenFreigabe: kostenFreigabe
    alt selected organisation has costs.read
        KostenFreigabe-->>ServerPage: gewaehert
        ServerPage-->>User: render KostenFlaeche
        KostenFlaeche->>KostenAPI: request costs data
    else missing permission or invalid selection
        KostenFreigabe-->>ServerPage: verboten or keine-auswahl
        ServerPage-->>User: render KostenGrenze
    end
Loading

Flow diagram for fail-closed organisation-scoped costs authorization

flowchart TD
    Start[Kosten route request] --> Gate[leseKostenFreigabe]
    Gate --> SessionState{Session state}
    SessionState -->|unbekannt| Unknown[kosten-sitzung-unbekannt]
    SessionState -->|abgemeldet| Unauthenticated[kosten-unauthenticated]
    SessionState -->|authenticated| Selection{Valid selected organisation?}
    Selection -->|no| Choose[kosten-org-auswahl]
    Selection -->|yes| Permission{Selected organisation has costs.read?}
    Permission -->|no| Forbidden[kosten-forbidden]
    Permission -->|yes| Granted[Render kosten-free loader]
    Granted --> Lazy[Load costs client chunks]
    Unknown --> End[No costs chunks]
    Unauthenticated --> End
    Choose --> End
    Forbidden --> End
Loading

File-Level Changes

Change Details Files
Kostenrouten erhalten ein fail-closed Server-Gate, das die Berechtigung der ausgewählten Organisation gegen die verifizierte Session prüft.
  • Führt eine reine Entscheidungslogik für angemeldet, abgemeldet, unbekannte Sitzung, fehlende Auswahl und fehlendes costs.read ein.
  • Liest Session und eyt_org serverseitig und behandelt den Cookie ausschließlich als validierten Selector, nicht als Autorisierungsquelle.
  • Gated /kosten und /kosten/stundensaetze vor jedem Kosteninhalt und rendert differenzierte Verweigerungsflächen.
  • Stellt beide Routen auf dynamisches Rendering um.
apps/web/app/(werkbank)/kosten/page.tsx
apps/web/app/(werkbank)/kosten/stundensaetze/page.tsx
apps/web/components/kosten-grenze.tsx
apps/web/lib/kosten-freigabe.ts
apps/web/test/kosten-freigabe.test.ts
apps/web/test/kosten-page-gate.test.tsx
Verhindert das Laden und Referenzieren von Kosten-Client-Chunks in verweigerten Zuständen.
  • Entkoppelt die Routen von den Kostenkomponenten über kostenfreie Loader mit next/dynamic.
  • Lädt Kostenansicht, Zugangsschutz und Stundensatzverwaltung erst im gewährten Client-Pfad.
  • Misst Chunk-Anforderungen und HTML-Referenzen in Auth-Journey und Shell-Smoke; ergänzt Positivkontrollen für gewährte Zugriffe.
  • Sichert die Architekturregel ab, dass jede Kosten-Seite das Server-Gate importiert, einschließlich Nicht-Leerlauf-Prüfung und Rot-Fällen.
apps/web/components/kosten-flaeche.tsx
apps/web/components/stundensaetze-flaeche.tsx
apps/web/e2e/auth-journey/journey.pwtest.ts
apps/web/e2e/shell-smoke.spec.ts
apps/api/test/architecture/rules.ts
apps/api/test/architecture.test.ts
apps/api/test/architecture-red-case.test.ts
Verankert die organisationsbezogene Auswahl- und Cookie-Semantik über die gesamte Client-Server-Komposition.
  • Schreibt eyt_org zentral beim Organisationswechsel, aktualisiert Server Components per router.refresh() und löscht den Selector beim Logout.
  • Stellt die Auswahl nach Reload wieder her, meldet sie aber erst nach verifizierter Session und verwirft fremde IDs.
  • Ergänzt Cookie-Parsing, Attribute, Löschung und Provider-Zustandsübergänge durch Unit-Tests.
apps/web/app/providers.tsx
apps/web/lib/organisations-auswahl-cookie.ts
apps/web/lib/session-provider.tsx
apps/web/test/organisations-auswahl-cookie.test.ts
apps/web/test/session-provider-auswahl.test.tsx
apps/web/e2e/auth-journey/journey.pwtest.ts
Erweitert die Mehr-Organisations- und Sitzungsfehler-Nachweise einschließlich API-Verbindlichkeit.
  • Prüft Auswahl ohne Organisation, Auswahl einer nicht berechtigten Zweitorganisation, direkten Zugriff auf Stundensätze, Wechsel auf eine berechtigte Organisation, Reload und manipulierten Cookie.
  • Belässt die serverseitige API-Autorisierung unverändert und prüft den unabhängigen 403-Nachweis.
  • Führt transaktionale Fixture-Leihgabe und garantierte Rückgabe einer Zweitorganisation mit Prämissen- und Nachbedingungswächtern ein.
  • Verschiebt den gemeinsamen Server-Session-Helper und aktualisiert seine Importe.
apps/web/e2e/auth-journey/journey.pwtest.ts
apps/web/e2e/auth-journey/eyt113-zweitorg-an.sql
apps/web/e2e/auth-journey/eyt113-zweitorg-aus.sql
apps/web/lib/sitzung-server.ts
apps/web/lib/feld/sitzung-server.ts
apps/web/app/(werkbank)/page.tsx
apps/web/app/feld/layout.tsx
apps/web/test/sitzung-server.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

…u werfen

decodeURIComponent warf auf boesartiger/kaputter Prozentkodierung
(eyt_org=%) URIError — und die Kompositionswurzel liest den Selector beim
Client-Start, der Crash traefe jede Seite. Jetzt: kaputte Kodierung ergibt
null (keine Auswahl), kein Rueckfall, keine Normalisierung, keine Ausnahme.
TDD: Unit-Rot gegen die alte Implementierung gemessen (URIError), dann
gruen; Journey-Phase h beweist im echten Browser pageerror=0,
kosten-org-auswahl, keine neuen Kosten-Chunk-Anfragen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DYAI2025
DYAI2025 merged commit 0883682 into master Aug 29, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants