# D2 — Plattformweite Rollen-, Rechte- und Datenzugriffsmatrix **Projekt:** GRENZENLOSE FÖRDERUNG e.V. · **Modus:** READ-ONLY · **Stand:** 2026-11-14, Branch `main`, HEAD `d1b719b`, `git diff HEAD = 0` **Grundlagen:** D0-Audit, D1/D1.1-Berichte, abgeglichen mit dem tatsächlichen Quellcode (`app/roles.py`, `app/main.py`, `app/routers/*.py`, `app/models.py` mit Zeilenangaben). **Dieses Dokument beschreibt ausschließlich den Ist-Stand und eine Soll-Vorschlag — es ändert nichts.** --- ## 1. Executive Summary - **Rollenmodell:** 4 produktive Rollen (`mitglied | vorstand | kassenpruefer | admin`) + Vorstandsfunktionen (`vorsitz1 | vorsitz2 | kasse | beisitz | protokoll`) — belegt in `app/roles.py:16-17` (Satzung §§ 6, 12–14). **„Team“ ist keine Rolle**, sondern die öffentliche Seite `/team` (`public/team.html`). - **Autorisierung ist durchgängig serverseitig:** jede der 62 Routen trägt eine `Depends`-Prüfung (`require_login`/`require_role`/`require_kasse`/`require_vorstand`/`require_vollvorstand`/`require_kassenpruefer`) — **0 Routen ohne Prüfung** außer: 5 öffentliche Seiten, Login/Registrierung, Logout, Webhook (Signatur) und Gate-Flag. - **Vier-Augen-Prinzip ist im Code erzwungen** (`kasse.py:275-297`): Anleger-Sperre, Doppelfreigabe-Sperre, zwei verschiedene `freigegeben_von_1/2`, Statuswechsel erst bei vollständiger Freigabe, Audit-Eintrag. - **Drei Kernbefunde:** 1. **Admin kann freigeben** (`require_kasse` lässt `admin` ohne Funktionenprüfung durch) — im **Widerspruch** zur geplanten Admin-Sonderregel (keine automatische Auszahlungsfreigabe). 2. **Mitglieder-PDF-Nachweis fehlt:** die Zuwendungsbestätigung existiert nur unter `/kasse/bestaetigung/{nr}.pdf` (`require_kasse`) — für Mitglieder gibt es keine Route (bekannter 404). 3. **Der DSGVO-Vollexport (`/admin/export`) wird nicht protokolliert** (kein `log()` im Block `admin.py:273-316`). - **Kernschutz bestätigt:** Mitglieder-Owner-Filter überall vorhanden (`mitglied.py:29-30,113,145,176-179`), Prüfer-Router ohne einzigen Schreib-Endpunkt (`pruefung.py:1-3`), Befangenheit + Gremium-2+1 erzwungen (`vorstand.py:84,102,138,148`), CSRF-Middleware auf allen POST (`main.py:66-71`), Rate-Limit nur mit Fehlversuchen (`auth.py:33,55-57`). ## 2. Tatsächlicher Ist-Zustand ### 2.1 Authentifizierung und Middleware (`app/main.py`) | Mechanismus | Fundstelle | Wirkung | |---|---|---| | Session-Cookie (Token-Hash, `revoked`, `expires_at`, `status=aktiv`) | `roles.py:31-46` | serverseitige Sitzung | | `require_login` → 303 `/login` | `roles.py:48-51` | Zugangsdeckel | | CSRF-Guard (alle POST, sonst Redirect 303) | `main.py:66-71` | CSRF-Schutz | | Security-Header (nosniff, X-Frame DENY, Referrer, HSTS) | `main.py:54-64` | Kopfschutz | | HEAD=GET (RFC 7231) | `main.py:38-51` | technisch | | Login-Rate-Limit (IP + E-Mail, **nur Fehlversuche**, Reset bei Erfolg) | `auth.py:33,55-68` | Brute-Force-Schutz | | Registrierung öffentlich → `role=mitglied, status=antrag` (nicht aktiv) | `auth.py:111-139` | Freischaltung erst nach Aufnahme | ### 2.2 Routenbestand (62 Routen, frisch gezählt) | Datei | Routen | Rechteprüfung | |---|---|---| | `public.py` | 9 | keine (öffentlich: Start, Team, Impressum, Datenschutz, Richtlinie, Beitragsordnung, Spenden, Danke) | | `auth.py` | 5 | Login-Logik + Rate-Limit | | `kasse.py` | 14 | `require_kasse` (14/14) | | `vorstand.py` | 10 | 8× `require_vorstand`, 4× `require_vollvorstand` (Überschneidung: Aufnahme/Einladung) | | `admin.py` | 12 | 12× `require_role("admin")` | | `mitglied.py` | 9 | 9× `require_role("mitglied")` | | `pruefung.py` | 1 | `require_kassenpruefer` (Router-Docstring: „STRIKT read-only — kein einziger Schreib-Endpunkt“) | | `api.py` | 2 | Webhook: Stripe-Signatur (`api.py:24`); Gate: öffentliches Flag | ### 2.3 Rechteprüfungen (`app/roles.py`) - `require_kasse` = `vorstand|admin`, bei `vorstand` zusätzlich Funktion ∈ {kasse, vorsitz1, vorsitz2} (`roles.py:64-70`) - `require_vorstand` = `vorstand|admin` (`roles.py:61`) - `require_vollvorstand` = nur Vorsitz/Kasse — Satzung: Aufnahme (§6), MV-Einladung (§11), Kassenfreigabe (§12) (`roles.py:72-82`) - `require_kassenpruefer` = `kassenpruefer|admin` (`roles.py:85`) - `is_foerdergremium` = nur gewählte Vorstände + Beisitz, **Admin ohne Vorstandsfunktion entscheidet nicht mit** (`roles.py:87-95`) - `check_befangenheit` → 409 bei eigenem Antrag (`roles.py:97-101`) - `check_kassentrennung` → Prüfer nie Vorstand (`roles.py:103-107`) ## 3. Rollenübersicht | Rolle | Umfang laut Code | Sichtbare Startseite | |---|---|---| | **Vorstand** (5 Funktionen) | Anträge/Gremium, Vereinsübersicht, Protokolle; Vollvorstand: Aufnahme, Einladung; Kasse/Vorsitz zusätzlich Kassenbereich | `/vorstand` | | **Kasse** (Vorsitz/Kasse + Admin) | Buchungen, Freigaben, Salden, Beiträge, Spenden, Bestätigungen/PDF | `/kasse` | | **Prüfer** (`kassenpruefer`) | eine read-only Lesekonsole: 100 Buchungen, 100 Beiträge, 50 Spenden, 50 Bestätigungen, 50 Auszahlungen, Salden (`pruefung.py:12-33`) | `/pruefung` | | **Mitglied** | nur eigene Daten (User-ID/E-Mail-Filter), Antragstellung, DSGVO-Anfragen, Profil | `/mitglied` | | **Team** | **keine Rolle** — öffentliche Team-Seite | `/team` (öffentlich) | | **Admin** | Nutzerverwaltung (Rollen/Passwörter/Fotos), Einstellungen, Audit, DSGVO-Status, **Voll-JSON-Export** + alle Rechte aus `require_*` (weiterleitungsfähig) | `/admin` | ## 4. Vollständige Berechtigungsmatrix Legende: **L** Lesen · **B** Bearbeiten · **F** Freigeben/Entscheiden · **E** Export — Kennzeichnung: BESTÄTIGT / TECHNISCH VORHANDEN / OFFEN / FEHLT / WIDERSPRUCH | Bereich / Funktion | Vorstand | Kasse | Prüfer | Mitglied | Admin | Kennzeichnung & Grundlage | |---|---|---|---|---|---|---| | Öffentliche Seiten (Start, Team, Richtlinie …) | L | L | L | L | L | BESTÄTIGT (`public.py`, keine Prüfung nötig) | | Login/Registrierung | L/B | L/B | L/B | L/B | L/B | BESTÄTIGT (offen, `auth.py`; neue Konten `status=antrag`) | | **Kassen-Dashboard/Salden/Buchungen** | L (nur Kasse/Vorsitz) | L/B/F | — | — | L/B/F | BESTÄTIGT `require_kasse` (`kasse.py:44,111,175`); §12 | | **Buchung anlegen** | — | B | — | — | B | TECHNISCH VORHANDEN — kein Sonderrecht, aber Admin erbt (Punkt 10) | | **4-Augen-Freigabe** | L (Kasse/Vorsitz) | **F** | — | — | **F** ⚠ | **WIDERSPRUCH** zur geplanten Admin-Regel (§3 des Auftrags); Code `kasse.py:275-297` | | **Auszahlung markieren** | L | F | — | — | F ⚠ | TECHNISCH VORHANDEN (Admin), ebenda | | Beiträge verbuchen (`/beitrag/{id}/bezahlt`) | — | B | — | — | B ⚠ | TECHNISCH VORHANDEN (einstufig, §12-Offenstand) | | Spenden verbuchen + Bestätigung erstellen | — | B/E | — | — | B/E | TECHNISCH VORHANDEN (`kasse.py:377,399`) | | **Bestätigungs-PDF herunterladen** | — | E | **FEHLT** | **FEHLT** | E | **FEHLT** für Mitglied/Prüfer: nur `require_kasse` (`kasse.py:459-461`) — bekannter 404 im Mitgliederbereich | | **Vorstand: Anträge lesen/bewerten/entscheiden** | L/F (Gremium) | — | — | — | L ⚠ | BESTÄTIGT `is_foerdergremium` + Befangenheit (`vorstand.py:94-148`); Admin ohne Funktion entscheidet **nicht** mit (`roles.py:91`) | | Mitgliedsantrag aufnehmen / MV-Einladung | **nur Vollvorstand** | — | — | — | L/B ⚠ | BESTÄTIGT `require_vollvorstand` (`vorstand.py:205,240`) | | Protokolle anlegen | **jeder Vorstand** | — | — | — | B ⚠ | TECHNISCH VORHANDEN — ob nur `funktion=protokoll` darf: **OFFEN** (§12) | | **Prüfer-Lesekonsole** | — | — | **L** | — | L | BESTÄTIGT, read-only (`pruefung.py`, kein Write-Endpunkt); §14 | | **Mitglieder: eigene Beiträge/Anträge/Spenden/DSGVO** | — | — | — | L/B (eigene) | L/B ⚠ | BESTÄTIGT Owner-Filter (`mitglied.py:29-30,113,145,176-179`) | | **Mitglieder: fremde Daten** | — | — | — | **nicht möglich** | L ⚠ | BESTÄTIGT (403/404 durch Filter+Owner-Check) | | **Globale Berichte/Finanzübersichten** | L (Kasse/Vorsitz) | L | L | **nicht vorhanden** | L | BESTÄTIGT (D1.1 rollengetrennt; produktiv: Kassen-/Prüfseiten tragen `require_*`) | | **DSGVO-Voll-Export JSON** | — | — | — | — | **E** ⚠ | TECHNISCH VORHANDEN — umfangreichster Export, **nicht protokolliert** (`admin.py:273-316`) | | Nutzer/Rollen ändern, Passwort setzen | — | — | — | — | **B** ⚠ | TECHNISCH VORHANDEN (temporärer Zustand, §3) | | Audit-Log einsehen | — | — | — | — | L | BESTÄTIGT (`admin.py:241`) | | Einstellungen ändern (§9-Beiträge!) | — | — | — | — | B ⚠ | TECHNISCH VORHANDEN — **Beitragsbeträge änderbar ohne MV-Beschluss-Weg** → OFFEN (§9 Abs. 2) | | Webhook `/api/stripe/webhook` | — | — | — | — | — | BESTÄTIGT (HMAC-Signatur `api.py:24`, idempotent) | | Gate-Flag `/api/gate/bestaetigungen` | — | — | — | — | — | öffentlich, nur Boolean (kein Datensatz) | ## 5. Seiten- und Routenmatrix | Route(n) | Prüfung | Lesen darf | Bearbeiten darf | Bemerkung | |---|---|---|---|---| | `public.py` (9) | keine | alle | — | korrekt öffentlich | | `auth.py` (5) | Rate-Limit | alle | Registrierung (erzeugt `status=antrag`) | Login setzt Session | | `/kasse*` (14) | `require_kasse` | Kasse/Vorsitz/Admin | Buchung, Freigabe, Auszahlung, Beitrags-/Spendenverbuchung, Bestätigung | keine Prüfer-/Mitgliedsroute | | `/vorstand` (10) | `require_vorstand` (8), `require_vollvorstand` (2+1) | alle Vorstände | Gremium-Funktionen nur `is_foerdergremium`; Aufnahme/Einladung nur Vollvorstand | `darf_pruefen` im Template gesteuert | | `/pruefung` (1) | `require_kassenpruefer` | Prüfer/Admin | **kein Write im Router** | `limit(100/50)` — kein Paginierung (D0-Lücke) | | `/admin` (12) | `require_role("admin")` | Admin | Nutzer, Einstellungen, DSGVO, Export | Sonderrecht, siehe 10 | | `/mitglied*` (9) | `require_role("mitglied")` | **nur eigene Datensätze** | Profil, DSGVO, Antragstellung | Owner-Filter je Route | | `/api/stripe/webhook` | Signatur | System | verbucht Spenden (nur `spende_id`) | kein Rollenpfad | | `/api/gate/bestaetigungen` | keine | alle | — | nur Flag | **Direkte URL-Aufrufe:** servergerendert — jede URL ist eine eigene Route mit eigener Prüfung; es gibt keine Client-Seitige Navigation, die Rechte umgehen könnte. **Hash-Navigation** existiert nur im D1-Prototyp (rein clientseitig, Demo). ## 6. API- und Server-Action-Matrix | Endpunkt | Methode | Schutz | Schreibwirkung | Datenumfang | |---|---|---|---|---| | `/api/stripe/webhook` | POST | HMAC-Signatur (`verify_webhook`) + Event-Idempotenz | `spenden.offen→bezahlt`, Buchung 1000→1200 | nur Spenden-Metadaten | | `/api/gate/bestaetigungen` | GET | keine | keine | `{"bestaetigungen_erlaubt": bool}` | | 24 POST-Formularrouten | POST | **CSRF-Middleware** + `require_*` | Routing-abhängig | sessionbasiert | Keine JSON-API für Client-Seite, keine Server-Actions außer Webhook/Gate — **kein separates API-Rechte-Layer nötig**, da alle Mutationen Formular-POST mit `require_*` sind. ## 7. Datenzugriffsmatrix (Tabellen × Rolle) | Tabelle (FK) | Vorstand | Kasse | Prüfer | Mitglied | Admin | Filter-Nachweis | |---|---|---|---|---|---|---| | `users` (95) | nur Name bei Anträgen/Protokollen | Anleger-/Freigabenamen | Namen bei Auszahlungen | **nur eigene Zeile** | voll | `mitglied.py:29-30` | | `beitraege` (user_id) | — | voll (global) | voll (`limit 100`) | **nur `user_id == user.id`** | voll | `mitglied.py:29` | | `antraege` (user_id) | voll (Gremium-Arbeit) | — | — | **nur eigene + Owner-Check in Detail** (`:113`) | voll | `mitglied.py:30,113` | | `spenden` (email) | Übersicht via Kasse | voll | voll | **nur `email == user.email`** | voll | `mitglied.py:176` | | `zuwendungsbestaetigungen` (spende_id, empfaenger_name) | — | voll + PDF | voll (Liste), **PDF fehlt** | **nur `empfaenger_name == user.name`** (Listenansicht) | voll | `mitglied.py:179`, `kasse.py:459` | | `buchungen` (soll/haben_konto, angelegt/freigegeben_von_*) | nur Kasse/Vorsitz | voll | voll (`limit 100/50`) | — | voll | `kasse.py`, `pruefung.py:14` | | `konten` (8) | nur Kasse/Vorsitz (Salden) | voll | voll (Salden) | — | voll | `kasse.py:111`, `pruefung.py:30` | | `antrag_punkte` / `antrag_gremium` | Gremium | — | — | nur eigene Antragsansicht | voll | `vorstand.py:94-148` | | `protokolle` | alle Vorstände (lesen+anlegen) | — | — | — | voll | `vorstand.py:217,224` | | `audit_log` (2043) | — | — | — | — | **lesend** (`admin.py:241`) | nicht löschbar (kein Delete-Endpunkt) | | `datenschutz_antraege` | — | — | — | **nur `email == user.email`** (`:145,163`) | Status ändern (`admin.py:256`) | | | `sessions` / `stripe_events` | — | — | — | — | — | Systemtabellen, kein UI-Zugriff | **Keine globale Mitgliederliste** für Mitglieder/Prüfer/Vorstand (nur Admin `/admin/users`). **Keine Lösch-Endpunkte** für Fachdaten vorhanden (kein DELETE in den Routern). ## 8. Export- und Dokumentenrechte | Weg | Format | Rechte | Umfang | Bewertung | |---|---|---|---|---| | `/kasse/bestaetigung/{nr}.pdf` | PDF | `require_kasse` | eine Bestätigung | **FEHLT für Mitglied** (eigener Nachweis) und Prüfer (Doppelcheck ohne PDF) → kennzeichnen | | `/admin/export` | JSON (Art. 20) | Admin | **users (inkl. E-Mail), Beiträge, Spenden (Namen), Anträge, Buchungen, Bestätigungen** | TECHNISCH VORHANDEN; **nicht protokolliert**; keine zeitliche/ sachliche Begrenzung → Punkt 10 | | CSV/XLSX/ZIP | — | — | **existieren nicht** (D0: 0 Treffer) | FEHLT (D5-Roadmap) — künftige Exporte müssen sich am Rollen-Umfang orientieren | | Bild-/Datei-Uploads (Fotos) | — | Admin (`/user/{uid}/foto`) + eigene? | Profile | TECHNISCH VORHANDEN | **Regel für die Soll-Architektur:** Jeder Export erbt den Lesenumfang der Rolle, wird protokolliert (`audit_log`) und darf nur über eine serverseitig geprüfte Route laufen — niemals über direkte Datei-URLs. ## 9. Vier-Augen-Prinzip (belegt, `kasse.py:275-297`) | Regel | Umsetzung | Kennzeichnung | |---|---|---| | Status `zur_freigabe` vorausgesetzt | `if not b or b.status != "zur_freigabe": Redirect` | BESTÄTIGT | | Anleger darf nicht freigeben | `user.id == b.angelegt_von → ?fehler=anleger` | BESTÄTIGT | | Zweite Freigabe nur durch andere Person | `elif user.id == b.freigegeben_von_1 → ?fehler=doppelt` | BESTÄTIGT | | Vollständige Freigabe setzt `freigegeben_von_1/2` + Zeit + Status | `else`-Zweig | BESTÄTIGT | | Nachvollziehbarkeit | `log(..., "buchung_freigegeben", user_id, objekt_typ/id, ip)` | BESTÄTIGT | | Auszahlung nur nach Freigabe | `ausgezahlt` nur aus `status == "freigegeben"` | BESTÄTIGT | | **Keine Umgehung durch Sonderrechte** | `admin` unterliegt denselben Prüfungen, **hat aber selbst Freigaberecht** | **WIDERSPRUCH** zur geplanten Admin-Regel (siehe 10) — ein Bypass-Code existiert nicht | | Einnahme-Verbuchung (Beitrag/Spende) | **einstufig** `freigegeben` ohne zweite Freigabe | TECHNISCH VORHANDEN — fachlich/satzungsmäßig **OFFEN** (Zusatzprüfung §3.2) | ## 10. Admin-Sonderrechte **Derzeit existierende Admin-Rechte (belegt):** alle `require_*`-Pfade als `admin` (Kassenfreigabe, Vorstandsfunktionen, Prüferlesicht), `require_role("admin")` für: Nutzerliste, **Rollen-/Funktionsänderung**, Profil/Foto, **Passwort-Setzen**, Einstellungen (inkl. Beitragsbeträge §9), Audit-Ansicht, DSGVO-Status, **Voll-JSON-Export** (`admin.py:66-284`). **Sonderregel des Auftrags — Ist vs. Soll:** | Anforderung | Ist-Stand | Kennzeichnung | |---|---|---| | Admin ≠ automatisch uneingeschränkte Einsicht | **nein** — Admin sieht und exportiert alles | **WIDERSPRUCH** | | Keine automatische Buchungs-/Auszahlungsfreigabe | **doch** — `require_kasse` lässt Admin freigeben | **WIDERSPRUCH** | | Vier-Augen nicht umgehbar | umgehbar nicht möglich (Regeln greifen), aber Admin ist freigabefähiger Teil davon | TECHNISCH VORHANDEN | | Erweiterte Einsicht: autorisiert, begründet, zeitlich begrenzt, protokolliert | **nicht umgesetzt** — kein Begründungs-/Zeit-/Export-Log | **FEHLT** | | Spätere Einschränkung technisch vorgesehen | nicht vorgesehen (eine Rolle, ein Flag) | **FEHLT** (als Soll-Architektur, Abschnitt 13, geplant — **derzeitige Administration eigenmächtig unverändert**) | ## 11. Sicherheitslücken und Risiken (Ist-Analyse, keine Tests durchgeführt) | # | Befund | Schwere (Vorschlag) | Beleg | |---|---|---|---| | S1 | Admin kann Freigeben/Verbuchen — geplant: getrennte technische Administration | hoch (Architektur) | `roles.py:64-70`, `kasse.py:275` | | S2 | `/admin/export` nicht protokolliert, unbegrenzter Umfang | hoch | `admin.py:273-316` (kein `log()`) | | S3 | Kein Mitglieder-PDF-Weg für eigene Bestätigungen (404) | mittel (Funktion) | `kasse.py:459` + Zusatzprüfung §2.5 | | S4 | Prüfer ohne PDF-Zugriff, aber mit Listen (Doppelcheck nur teilweise möglich) | mittel | `pruefung.py` | | S5 | Einstellungen (Beitragsbeträge §9) ohne Beschluss-/Audit-Pflicht änderbar | mittel | `admin.py:214` | | S6 | Protokoll-Anlage durch **jeden** Vorstand (nicht nur `funktion=protokoll`) | niedrig/mittel | `vorstand.py:224` | | S7 | Einstufige Einnahme-Verbuchung (ohne 2. Freigabe) | fachlich OFFEN | `kasse.py:334,377` | | S8 | Kein Prüfstatus-Feld → „abgeschlossene Prüfung“ nicht abbildbar | mittel (Datenmodell) | D0 §9 | | S9 | Keine Paginierung in Prüfung/Kassenlisten (100er-Limits) | niedrig | `pruefung.py:14-24` | | S10 | Login-Logout als GET-Redirect (bekannter Alt-Offenstand, dokumentiert) | niedrig | `auth.py:97` | **Geplante Negativtests (DOKUMENTIERT, NICHT AUSGEFÜHRT — Auftragsgrenze):** 1. Direkter URL-Aufruf `/kasse*` als Mitglied/Prüfer → erwartet 403/303. 2. `/admin*` als Kasse/Vorstand → 403. 3. `/mitglied/antraege/{fremde_id}` mit eigenem Login → erwartet 404 (Owner-Check `:113`). 4. `/kasse/bestaetigung/{fremde_nummer}.pdf` als Mitglied → 303/403. 5. `/admin/export` als Nicht-Admin → 403. 6. POST ohne CSRF-Token → Redirect (Middleware). 7. Zweite Freigabe durch dieselbe Person → `?fehler=doppelt`. 8. Freigabe durch Anleger → `?fehler=anleger`. 9. Login-Rate-Limit (5 Fehlversuche) → Sperre. 10. Webhook ohne/falsche Signatur → 400 + Audit. 11. Session abgelaufen/revoked → 303 `/login`. 12. Registrierter `status=antrag`-Account → keine freigabefähigen Ansichten. ## 12. Offene Vereinsentscheidungen Vollständige, begründete Liste: **`docs/D2-OFFENE-VEREINSENTSCHEIDUNGEN.md`** (dort einzige Fundstelle; hier nicht eigenmächtig entschieden). ## 13. Soll-Architektur (Vorschlag, keine Umsetzung) 1. **Rollen bleiben die4 + Funktionen** (keine neuen produktiven Rollen). „Team“ = öffentliche Seite, keine Rolle. 2. **Technische Administration abtrennen:** neue interne Kennzeichnung `tech_admin` (oder Flag), die **keinen** automatischen `require_kasse`-/`require_vorstand`-Pfad öffnet. Zugriff auf Vereins-/Finanz-/Mitgliedsdaten nur über **zeitlich befristete, begründete, protokollierte Freigabe-Einträge** (Einsichts-Job mit Ablaufdatum, immer mit `audit_log`-Eintrag `admin_einsicht_gewaehrt/abgelaufen`). 3. **Freigabe-Wege:** Auszahlungsfreigabe ausschließlich über `require_kasse` **ohne** Admin; Admin behält: Nutzer-/Technik-Wartung, Audit-Lesicht, DSGVO-Status — Export nur mit Pflichtbegründung + Log. 4. **Least Privilege je Route:** bestehende `require_*` beibehalten; neue Kriterien als kleine prüfbare Funktionen (`require_kassenfunktion_ohne_admin()` o. ä.), **Freigabelogik byteidentisch**. 5. **Need-to-know-Daten:** Antragsdetail für Vorstand reduzierbar auf fachliche Felder (Name nur für Gremium/Namensnennung) — Entscheidung erforderlich (offen). 6. **Exporte:** künftige CSV/XLSX/ZIP erben den Rollenumfang, laufen über eine Export-Route mit `audit_log`, nie über direkte Datei-Pfade; PDF-Wege für Mitglied (eigene) und Prüfer (Doppelcheck) ergänzen. 7. **Datenmodell:** Prüfstatus-Feld (D6) und `beleg_ref`/`antrag_id`-Pflicht (D4) als Voraussetzung für belastbare Prüf-/Nachweisberichte. 8. **Vier-Augen bleibt unantastbar:** keine Route, kein Flag und kein Admin-Weg darf eine Freigabe ohne zwei verschiedene Personen ermöglichen (Regressionstest als Abnahmekriterium). ## 14. Priorisierte Umsetzungsempfehlungen (nach Freigabe) | Prio | Maßnahme | Bezug | |---|---|---| | 1 | Admin aus Freigabe-/Verbuchungspfaden ausschließen (eigene Dependency), Logik der 4-Augen unverändert | S1, §3 | | 2 | `/admin/export` mit Pflichtbegründung + `audit_log`-Eintrag | S2 | | 3 | Mitglieder-PDF-Weg für eigene Zuwendungsbestätigungen (Owner-geprüft) | S3 | | 4 | Prüfer-PDF-Zugriff für Doppelcheck | S4 | | 5 | Export-/Einsichtsberechtigung mit Zeitbegrenzung (Soll-Architektur 2) | §3, S1 | | 6 | Beitragsbeträge nur über dokumentierten MV-Beschluss-Weg änderbar | S5, §9 | | 7 | Protokoll-Anlage auf `funktion=protokoll` begrenzen (falls bestätigt) | S6 | | 8 | Prüfstatus-Modell + Belegreferenzen (D4/D6) | S8 | | 9 | Paginierung in Prüfungs-/Kassenlisten | S9 | ## 15. Testkonzept und Abnahmekriterien **Abnahmekriterien:** alle in §11 dokumentierten Negativtests bestanden; 4-Augen-Regression (Anleger/Doppelt-Sperre) unverändert grün; Export-Umfang ≤ Rollenumfang; kein Admin-Pfad ohne Freigabelogik; Mitglieder sehen nur eigene Datensätze (je Route); jedes Audit-ereignis mit `user_id/objekt/ip`; keine Widersprüche mehr zwischen Admin-Sonderregel und Implementierung. **Vorgehen nach Freigabe:** Negativtests gegen eine Testinstanz mit Testfixtures (kein Produktivdatensatz), danach Screenshot-/Statusprotokoll je Testfall, Freigabe durch den Vorstand vor jeder Umsetzung. **Keine der Maßnahmen wurde umgesetzt — D2 endet mit diesem Dokument.** --- **Integrität:** `git diff HEAD = 0`, 0 geänderte tracked Dateien, keine DB-Zugriffe mit Schreibwirkung, keine D1-/D1.1-Änderung, keine Installation. READ-ONLY eingehalten.