diff --git a/docs/adr/0005-keycloak-sso-with-dev-bypass.md b/docs/adr/0005-keycloak-sso-with-dev-bypass.md new file mode 100644 index 0000000..7a5de7a --- /dev/null +++ b/docs/adr/0005-keycloak-sso-with-dev-bypass.md @@ -0,0 +1,80 @@ +# 0005 — Keycloak SSO mit Dev-Bypass für Read/Write-Trennung + +| | | +|---|---| +| **Status** | accepted | +| **Datum** | 2026-04-10 | +| **Refs** | Issue #43, Commit 7159240 + 303b30f, app/auth.py | + +## Kontext + +Die Webapp soll öffentlich durchsuchbar sein (Assessments lesen, PDFs +ansehen, Auswertungen), aber Analyse-Aktionen (Antrag bewerten, Programme +indexieren) nur authentifizierten Nutzern erlauben. Der User betreibt +bereits einen Keycloak-Server unter `sso.toppyr.de` mit Realm `collaboration`. + +## Optionen + +### Option A — Keycloak-only mit hartem 401 + +Alle POST-Endpoints erfordern JWT. Ohne Keycloak-Server läuft nichts. + +**Vorteile:** Klar, sicher. +**Nachteile:** Lokale Entwicklung blockiert ohne Keycloak-Instanz. + +### Option B — Dev-Bypass: Auth nur wenn ENV gesetzt + +Wenn `KEYCLOAK_URL` leer ist → ALLE Endpoints offen (Dev-Modus). +Wenn gesetzt → POST-Endpoints erfordern JWT, GET bleibt offen. + +**Vorteile:** Lokale Dev ohne Keycloak. Prod sofort sicher per ENV. +**Nachteile:** Versehentlich Prod ohne ENV = alles offen. + +### Option C — API-Key statt Keycloak + +Einfacher API-Key-Header für POST-Endpoints. + +**Vorteile:** Zero-Dependency. +**Nachteile:** Kein SSO, kein User-Identity, keine Gruppen/Rollen. + +## Entscheidung + +**Option B.** Dev-Bypass ermöglicht reibungslose lokale Entwicklung und +Batch-Skripte (die keinen JWT haben). Prod-Absicherung über die drei +ENV-Vars in docker-compose.yml. Die Keycloak-Client-Registrierung in +`sso.toppyr.de` ist ein einmaliger manueller Schritt. + +## Implementation + +- `app/auth.py`: JWKS-Cache (1h TTL), `get_current_user` (optional), + `require_auth` (pflicht), `keycloak_login_url` (für Browser-Redirect) +- POST-Endpoints (`/analyze`, `/api/analyze-drucksache`, + `/api/programme/index`): `user: dict = Depends(require_auth)` +- GET-Endpoints: unverändert offen +- Frontend: `initAuth()` prüft `/api/auth/me`, steuert + "Jetzt prüfen"-Button (disabled + Tooltip wenn nicht eingeloggt) + und "Anmelden"-Button im Header + +## Konsequenzen + +### Positiv + +- Read-Only für alle, Write nur mit Login — klare Trennung +- Batch-Skripte und auto-Re-Analyse im Container laufen im Dev-Modus + (KEYCLOAK_URL nicht gesetzt), keine Auth-Hürde für Maintenance +- Keycloak-Rollen über `realm_access.roles` im JWT verfügbar für + künftige Gruppen-Features (#94 Bookmarks/Kommentare) + +### Negativ + +- Dev-Modus ist unsicher — wenn jemand KEYCLOAK_URL in Prod vergisst + zu setzen, ist alles offen. Mitigation: Health-Endpoint oder + Startup-Warning wenn ENV fehlt. +- JWKS-Cache (1h) bedeutet: nach Key-Rotation dauert es bis zu 1h bis + alte Tokens abgelehnt werden. Für dieses Projekt akzeptabel. + +### Folgen für andere ADRs + +- #94 Bookmarks/Kommentare baut auf der User-Identity aus dem JWT auf + (`sub`, `email`, `realm_access.roles`). +- #95 Queuing könnte Auth-User priorisieren. diff --git a/docs/reference/adapter-capabilities.md b/docs/reference/adapter-capabilities.md index fa8523e..b646711 100644 --- a/docs/reference/adapter-capabilities.md +++ b/docs/reference/adapter-capabilities.md @@ -55,6 +55,146 @@ Satz von Treffern nach WP/Datum/Typ und filtert dann im Python-Code per | **HE** | Perl-Data::Dumper in HTML-Comments, Hex-Escape-Decoding (`\x{e9}` → `é`) | | **NI** | ❌ **Nicht implementiert** — NILAS-StarWeb braucht Session-Cookie, HAR-Capture nötig | +## Feld-Mapping: Woher kommen die Daten? + +Pro Adapter: welches API-Feld wird zu welchem Drucksache-Feld, mit Beispiel. + +### NRW (NRWAdapter — OPAL HTML) + +| Feld | Quelle | Beispiel | +|---|---|---| +| title | HTML `.e-document-result-item__title` `` Text | "Kostenloses Parken für E-Fahrzeuge aufheben" | +| datum | `
` → Regex | "19/3104" | +| link | `` (http→https, relativ→absolut) | `https://pardok.parlament-berlin.de/...` | +| typ | `
` vor Fraktionsnamen | "Antrag", "Vorlage" | + +### MV / HH / TH (ParLDokAdapter — JSON-API) + +| Feld | Quelle (JSON-Feld) | Beispiel | +|---|---|---| +| title | `title` | "Weiterbildungsförderung" | +| datum | `date`, DD.MM.YYYY → ISO | "15.03.2026" → "2026-03-15" | +| fraktionen | `authorhtml` → `extract_fraktionen()` | "Klaus Meyer (CDU)" → `["CDU"]` | +| drucksache | `f"{lp}/{number}"` | lp=8, number=1594 → "8/1594" | +| link | `{base_url}{prefix}{link}` (Fragment `#navpanes=0` gestrippt) | `https://dokumentation.landtag-mv.de/parldok/dokument/8` | +| typ | `type` (TH: Substring-Match auf "Antrag") | "Antrag", "Antrag gemäß § 79 GO" | + +### SH (StarFinderCGIAdapter — Legacy CGI) + +| Feld | Quelle (HTML-Regex) | Beispiel | +|---|---|---| +| title | `` in Tabellenzeile | "Energiewende vorantreiben" | +| datum | Nach ``, DD.MM.YYYY → ISO | "07.04.2026" → "2026-04-07" | +| fraktionen | Urheber-Text → `extract_fraktionen()` | "Christian Dirschauer (SSW)" → `["SSW"]` | +| drucksache | `` Text | "20/5136" | +| link | `` direkt | `http://lissh.lvn.parlanet.de/.../20-5136.pdf` | +| typ | Fest: "Antrag" (server-side `dtyp=antrag`) | "Antrag" | + +### HE (StarWebHEAdapter — eUI Perl-Dump) + +| Feld | Quelle (Perl-Dump-Feld) | Beispiel | +|---|---|---| +| title | `WEV01[0].main` (Hex-Escape-Decoding `\x{e9}` → `é`) | "Schulinfrastruktur modernisieren" | +| datum | `WEV02[0].main`, DD.MM.YYYY → ISO | "29.02.2026" → "2026-02-29" | +| fraktionen | `WEV12[0].main` → `extract_fraktionen()` | "Klaus Dieter (GRÜNE)" → `["GRÜNE"]` | +| drucksache | `WEV08[0].main` | "21/8532" | +| link | `WEV07[0].main` (http→https) | `https://starweb.hessen.de/.../21_8532.pdf` | +| typ | `WEV03[0].main` | "Antrag", "Antrag mit Änderung" | + +### HB (PARiSHBAdapter — Java-Servlet HTML) + +| Feld | Quelle (HTML-Regex) | Beispiel | +|---|---|---| +| title | `

` | "Bremenpass für Kultur" | +| datum | Nach Drucksache, DD.MM.YYYY → ISO | "23.02.2026" → "2026-02-23" | +| fraktionen | Nach Datum → `extract_fraktionen()` | "SPD, BÜNDNIS 90/DIE GRÜNEN, Die Linke" → `["SPD", "GRÜNE", "LINKE"]` | +| drucksache | `Drs 21/730` + optionaler Suffix (S/L) | "21/730", "21/730S" | +| link | `` | `https://www.bremische-buergerschaft.de/.../21/730.pdf` | +| typ | Regex vor Datum | "Antrag", "Änderungsantrag" | + +### BW (PARLISAdapter — eUI async Polling, JSON-in-HTML) + +| Feld | Quelle (Perl/JSON-Feld) | Beispiel | +|---|---|---| +| title | `WMV33` (Schlagworte, `` gestrippt) / Fallback `EWBV23` | "Energiewirtschaft; Stromversorgung" | +| datum | `EWBV23` → Regex DD.MM.YYYY → ISO | "16.03.2026" → "2026-03-16" | +| fraktionen | `WMV30` (Kurz-Urheber) → `extract_fraktionen()` | "Felix Herkens (GRÜNE) u. a." → `["GRÜNE"]` | +| drucksache | `EWBV22` oder `EWBD01` → Regex | "17/10323" | +| link | `EWBD05[0].main` (direkte PDF-URL) | `https://parlis.landtag-bw.de/.../17_10323.pdf` | +| typ | Fest: `document_typ="Antrag"` (in `lines.l4`) | "Antrag" | + +### BY (BayernAdapter — TYPO3-Solr HTML) + +| Feld | Quelle (HTML-Regex) | Beispiel | +|---|---|---| +| title | `
` | "Kostenloses Parken für E-Fahrzeuge aufheben" | +| datum | `

` "Drucksache Nr. 19/11407 vom 08.04.2026" DD.MM.YYYY → ISO | "08.04.2026" → "2026-04-08" | +| fraktionen | `

` "Antrag AfD" → `extract_fraktionen()` | "Antrag CSU, FREIE WÄHLER" → `["CSU", "FW-BAYERN"]` | +| drucksache | `

` Regex | "19/11407" | +| link | `` (absolute URL) | `https://www.bayern.landtag.de/.../0000009107.pdf` | +| typ | Erstes Wort aus `

` | "Antrag" | + +### SL (SaarlandAdapter — Umbraco JSON) + +| Feld | Quelle (JSON-Feld) | Beispiel | +|---|---|---| +| title | `Title` | "Schule als Lern- und Bildungsort weiter stärken" | +| datum | `PublicDate` ISO-Format, erste 10 Zeichen | "2022-05-12T00:00:00" → "2022-05-12" | +| fraktionen | `Publisher` + `DocumentAuthor` → `extract_fraktionen()` | Publisher "CDU" → `["CDU"]` | +| drucksache | `DocumentNumber` | "17/11" | +| link | `FilePath` mit `/file.ashx` → `/Downloadfile.ashx` Rewrite | `https://www.landtag-saar.de/Downloadfile.ashx?FileId=14230&FileName=Ag17_0011.pdf` | +| typ | `DocumentType` | "Antrag", "Anfrage", "Gesetzentwurf" | + +### SN (SNEdasXmlAdapter — Manueller XML-Export) + +| Feld | Quelle (XML-Element) | Beispiel | +|---|---|---| +| title | `` (CDATA) | "Geschäftsordnung des Sächsischen Landtags" | +| datum | `` → Regex "datum"-Gruppe, DD.MM.YYYY → ISO | "01.10.2024" → "2024-10-01" | +| fraktionen | `` → Regex "urheber"-Gruppe → `extract_fraktionen()` | "CDU, BSW, SPD" → `["CDU", "BSW", "SPD"]` | +| drucksache | `f"{Wahlperiode}/{Dokumentennummer}"` | "8/2" | +| link | Konstruiert: `.../viewer.aspx?dok_nr={nr}&dok_art=Drs&leg_per={wp}` (PDF wird on-demand aus Iframe gelöst) | `https://edas.landtag.sachsen.de/.../viewer.aspx?dok_nr=2&dok_art=Drs&leg_per=8` | +| typ | Fest: "Antrag" | "Antrag" | + +### BUND (BundestagAdapter — DIP REST-API) + +| Feld | Quelle (JSON-Feld) | Beispiel | +|---|---|---| +| title | `titel` | "Förderung von Genossenschaften im sozialen Bereich" | +| datum | `datum` (bereits ISO YYYY-MM-DD) | "2026-04-08" | +| fraktionen | `urheber[*].titel` → `extract_fraktionen()` | "Fraktion der AfD" → `["AfD"]` | +| drucksache | `dokumentnummer` | "21/5136" | +| link | `fundstelle.pdf_url` | `https://dip.bundestag.de/documents/btd/21/051/2105136.pdf` | +| typ | `drucksachetyp` (server-side gefiltert) | "Antrag" | + +## Gemeinsames Pattern + +Alle Adapter normalisieren Fraktionen über den zentralen `parteien.extract_fraktionen(text, bundesland=...)` Mapper (#55). Alle konvertieren DE-Datumsformat (DD.MM.YYYY) nach ISO (YYYY-MM-DD), außer BUND (schon ISO) und SL (schon ISO mit T-Suffix). + ## Historien-Tiefe (ältere WPs) Alle Adapter sind aktuell auf **eine feste Wahlperiode** konfiguriert.