diff --git a/dms-kanban/board.csv b/dms-kanban/board.csv index ca0ec7c9..7469f65e 100644 --- a/dms-kanban/board.csv +++ b/dms-kanban/board.csv @@ -455,3 +455,18 @@ Bei Produktion oder fehlender Markierung wird das Flag ignoriert Deployment-Checkliste enthält Entfernungsschritt vor Produktivbetrieb","Angriffsnachstellung: Flag ohne Nicht-Produktions-Markierung, vor/nach Fix dokumentiert Regressionstest: Flag mit korrekter Markierung funktioniert für Host 131 weiterhin Test: Flag ohne jede Umgebungsvariable wird ignoriert" +SEC-08,"Objektrechte (Ordner-ACL) existieren im Code, werden aber nie geprueft",🔒 Sicherheit,Backend,Komplex,3,13,Backlog,ObjectAllowed in authz verdrahten,"PoolChecker entscheidet Sichtbarkeit nur ueber created_by == caller plus statische Admin-Liste. policyclient.ObjectAllowed (Core RBAC-11, Ordner-/Dokumentpfad inkl. Gruppen) existiert fertig implementiert, wird aber nirgends aufgerufen. Suche filtert ebenfalls hart auf created_by.",,,"CanAccessDocument/CanAccessFolder fragen zusaetzlich ObjectAllowed ab, hasEntries=false aendert nichts am bisherigen Verhalten +Bei hasEntries=true entscheidet ausschliesslich ObjectAllowed +Suche filtert bei vorhandenen Objektrechten auf erlaubte Ordner-IDs statt created_by +Ausfall der Objektrechte-Abfrage fuehrt zu Ablehnung (fail-closed) +groupIDs-Herkunft dokumentiert","Mandant ohne Objektrechte-Eintrag: Verhalten identisch zum Stand vorher +Dokument mit Freigabe fuer fremden Nutzer: dieser kann nun zugreifen +Dokument mit Entzug fuer Ersteller: Ersteller wird abgelehnt +RBAC-11 nicht erreichbar: Zugriff verweigert +Suche liefert nur erlaubte Treffer" +SEC-09,CMIS-Autorisierungspfad geprueft: kein Bypass bestaetigt,🔒 Sicherheit,Backend,Mittel,2,13,Fertig,CMIS-Zugriffspruefung geklaert,"Vermuteter Bypass geprueft und per echtem Test (TestPermissionParityWithREST, Host 204, TEST_TENANT_DSN) widerlegt: CMIS-Backend ist auf die komplette Middleware-Kette verdrahtet, interner Re-Dispatch prueft CanAccessDocument/CanAccessFolder erneut. Kein Code-Fix noetig.",,,"Dokumentiert und per Test belegt, dass CMIS-Zugriff auf fremdes Dokument NICHT durchgeht - erledigt +Kein Code-Fix erforderlich, da kein Bypass bestaetigt +Verbleibendes AllowCaller-Restrisiko als bereits durch SEC-07 abgedeckt dokumentiert","Ausgangsmessung durchgefuehrt: bestehender Test TestPermissionParityWithREST auf Host 204 gegen echte migrierte Testdatenbank ausgefuehrt, PASS +Fremder Nutzer mit gueltigem Token liest fremdes Dokument ueber CMIS: 403 bestaetigt +Gefaelschter caller-Parameter neben echtem fremden Token: 403 bestaetigt (Token-Identitaet gewinnt) +REST und CMIS liefern fuer alle Tokenzustaende identische Statuscodes" diff --git a/dms-kanban/board.json b/dms-kanban/board.json index 9f007b3f..d3b9ba24 100644 --- a/dms-kanban/board.json +++ b/dms-kanban/board.json @@ -3468,6 +3468,67 @@ "wave": 13, "unlocks": [], "prompt": "# SEC-07 – NEXARCH_DMS_APP_ALLOW_CALLER_IDENTITY umgeht komplette Authentifizierung\n\n**Epic:** 🔒 Sicherheit | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 2 Personentage | **Welle:** 13\n\n---\n\n## Rolle\nDu bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH DMS (Dokumentenkern, Suche & OCR, Metadaten, Versionierung, Integration/API), das sich an paperless-ngx, Alfresco, Docspell und ecoDMS orientiert und bekannte Fehler aus archivdms (siehe known-issues-archivdms.md) bewusst vermeidet.\nServerseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.\n\n## Auftrag\nSecurity-Review (main, Fable 5.1) hat einen Befund mit mittlerem Schweregrad gefunden: `dms/internal/moduleauth/middleware.go:46-50` und `policyclient.go:174-176` erlauben über das Flag `NEXARCH_DMS_APP_ALLOW_CALLER_IDENTITY=true` Admin-Zugriff über einen reinen Query-Parameter, komplett ohne echte Authentifizierung. Laut `DEPLOYMENT-131-CHECKLISTE.md:40` ist dies \"übergangsweise\" für Host 131 vorgesehen; ein übersehenes Flag in einer produktionsnahen Umgebung wäre ein vollständiger Auth-Bypass.\n\n**Technische Grundlage:** `internal/moduleauth/middleware.go`, `internal/moduleauth/policyclient.go`, `DEPLOYMENT-131-CHECKLISTE.md`\n\n## Produkt-DNA für diesen Bereich\n**Übernehmen:**\n- Übergangslösungen für Testumgebungen dürfen existieren, müssen aber technisch auf diese Umgebung beschränkt sein, nicht nur dokumentarisch.\n\n**Bewusst vermeiden:**\n- Ein reines Deployment-Checklisten-Item als einzige Absicherung gegen einen vollständigen Auth-Bypass in Produktion.\n\n## Ausgangslage\nDas Flag ist aktuell rein konfigurationsbasiert und ohne zusätzliche Laufzeit-Schranke wirksam, sobald gesetzt — unabhängig von der tatsächlichen Umgebung, in der der Dienst läuft.\n\n## Akzeptanzkriterien\nDie Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:\n1. Das Flag wirkt nur, wenn zusätzlich ein expliziter Umgebungs-/Hostname-Check bestätigt, dass es sich um eine als Nicht-Produktion markierte Umgebung handelt (z. B. über eine separate, eindeutig benannte Umgebungsvariable wie `NEXARCH_ENV=development`)\n2. Ist die Umgebung als Produktion markiert oder die Markierung fehlt, wird das Flag ignoriert und die reguläre Authentifizierung bleibt zwingend\n3. `DEPLOYMENT-131-CHECKLISTE.md` enthält einen expliziten Schritt, das Flag vor Produktivbetrieb zu entfernen bzw. die Nicht-Produktions-Markierung zu prüfen\n\n## Prüfungen vor der Abnahme\nFühre jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:\n1. Angriffsnachstellung: Flag gesetzt, aber Umgebung nicht als Nicht-Produktion markiert — Aufruf mit reinem Query-Parameter-Identitätsvortäuschung — vor dem Fix erfolgreich als Admin, nach dem Fix mit regulärer Authentifizierungspflicht abgelehnt (beide Ergebnisse dokumentieren)\n2. Regressionstest: Flag gesetzt UND Umgebung korrekt als Nicht-Produktion markiert — Übergangsverhalten für Host 131 funktioniert weiterhin wie vorgesehen\n3. Test: Flag gesetzt, Umgebungsvariable fehlt komplett — Flag wird ignoriert\n\n## Nicht Bestandteil dieser Kachel\nDiese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:\n- `SEC-06` TENANT_SLUG-Pflicht (verwandter, aber eigenständiger Befund im selben Modul)\n- Vollständige Ablösung der Übergangslösung für Host 131 (eigenständiges, größeres Vorhaben)\n\n## Arbeitsweise\n1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.\n2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.\n3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.\n4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.\n5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.\n6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.\n\n## Abschluss\n- Commit-Nachricht: `SEC-07: allow-caller-identity-produktionssperre`\n- Branch: `feature/sec-07-allow-caller-identity-produktionssperre`\n- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.\n- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.\n" + }, + { + "code": "SEC-08", + "title": "Objektrechte (Ordner-ACL) existieren im Code, werden aber nie geprueft", + "epic": "SEC", + "featureIds": [], + "role": "Backend", + "complexity": "Komplex", + "estimateDays": 3, + "tech": "ObjectAllowed in authz verdrahten", + "description": "PoolChecker entscheidet Sichtbarkeit nur ueber created_by == caller plus statische Admin-Liste. policyclient.ObjectAllowed (Core RBAC-11, Ordner-/Dokumentpfad inkl. Gruppen) existiert fertig implementiert, wird aber nirgends aufgerufen. Suche filtert ebenfalls hart auf created_by.", + "dependsOn": [], + "akzeptanz": [ + "CanAccessDocument/CanAccessFolder fragen zusaetzlich ObjectAllowed ab, hasEntries=false aendert nichts am bisherigen Verhalten", + "Bei hasEntries=true entscheidet ausschliesslich ObjectAllowed", + "Suche filtert bei vorhandenen Objektrechten auf erlaubte Ordner-IDs statt created_by", + "Ausfall der Objektrechte-Abfrage fuehrt zu Ablehnung (fail-closed)", + "groupIDs-Herkunft dokumentiert" + ], + "pruefungen": [ + "Mandant ohne Objektrechte-Eintrag: Verhalten identisch zum Stand vorher", + "Dokument mit Freigabe fuer fremden Nutzer: dieser kann nun zugreifen", + "Dokument mit Entzug fuer Ersteller: Ersteller wird abgelehnt", + "RBAC-11 nicht erreichbar: Zugriff verweigert", + "Suche liefert nur erlaubte Treffer" + ], + "status": "Backlog", + "epicName": "Sicherheit", + "epicEmoji": "🔒", + "wave": 13, + "unlocks": [], + "prompt": "# SEC-08 – Objektrechte (Ordner-ACL) existieren im Code, werden aber nie geprüft\n\n**Epic:** 🔒 Sicherheit | **Rolle:** Backend | **Komplexität:** Komplex | **Aufwand:** 3 Personentage | **Welle:** 13\n\n---\n\n## Rolle\nDu bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH DMS (Dokumentenkern, Suche & OCR, Metadaten, Versionierung, Integration/API), das sich an paperless-ngx, Alfresco, Docspell und ecoDMS orientiert und bekannte Fehler aus archivdms (siehe known-issues-archivdms.md) bewusst vermeidet.\nServerseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.\n\n## Auftrag\nVergleichsreview gegen die vier Referenzprojekte (Fable 5.1) hat eine strukturelle Lücke gefunden, die paperless-ngx und Docspell ebenfalls haben und die NEXARCH laut `dms/README.md` bewusst vermeiden wollte: `internal/authz.PoolChecker` entscheidet Sichtbarkeit ausschließlich über `created_by == caller` (`internal/authz/authz.go:36-67`) plus eine statische Admin-Liste/-Rolle. `internal/policyclient.Client.ObjectAllowed` (`internal/policyclient/policyclient.go:104`) implementiert bereits eine Core-RBAC-11-Anfrage für Objektrechte entlang eines Ordner-/Dokumentpfads inklusive Gruppen (`groupIDs`) — wird aber an keiner Stelle im Produktionscode aufgerufen (`grep -r ObjectAllowed` außerhalb von `policyclient.go` selbst liefert nichts in `apiwiring`/`cmd`). Die Suche filtert ebenfalls hart auf den Ersteller (`internal/searchapi/handler.go:118`, `FilterCreatedBy`). Ergebnis: Freigabe eines Dokuments/Ordners an andere Nutzer oder Gruppen ist im DMS schlicht nicht möglich, trotz vorhandener Schnittstelle dafür.\n\n**Technische Grundlage:** `internal/authz/authz.go`, `internal/policyclient/policyclient.go` (`ObjectAllowed`, `ObjectPathNode`), `internal/searchapi/handler.go`, Migration 0006 (materialized path für Ordner)\n\n## Produkt-DNA für diesen Bereich\n**Übernehmen:**\n- Alfresco: echte Objekt-/Ordner-ACLs, Freigabe an Nutzer und Gruppen, nicht nur Ersteller.\n\n**Bewusst vermeiden:**\n- paperless-ngx/Docspell: Sichtbarkeit faktisch nur über Eigentümerschaft, kein Teilen/Delegieren möglich.\n\n## Ausgangslage\n`ObjectAllowed` existiert fertig implementiert inklusive `hasEntries`-Rückwärtskompatibilitätssignal (kein Objektrechte-Eintrag entlang des Pfads = wie bisher verhalten), ist aber nicht verdrahtet. Zwei offene Vorfragen, bevor Umsetzung sinnvoll ist (siehe unten).\n\n## Offene Fragen vor Umsetzungsbeginn\nDiese müssen geklärt sein, bevor die Akzeptanzkriterien final feststehen — bei Unklarheit nachfragen, nicht raten:\n1. **Herkunft von `groupIDs`:** `moduleauth.Identity` (`internal/moduleauth/middleware.go:12-15`) hat nur `UserID`, `Tenant`, `Role` — keine Gruppenmitgliedschaften. Muss das Modul-JWT um einen `groups`-Claim erweitert werden (Core-seitige Änderung, andere Kachel?), oder gibt es bereits eine andere Quelle für Gruppenzugehörigkeit?\n2. **Pfadauflösung:** Für `ObjectAllowed` wird der volle Pfad von der Wurzel bis zum Objekt gebraucht (`[]ObjectPathNode`). Migration 0006 legt einen materialized path für Ordner an — reicht eine rekursive Abfrage über `folders.path`, oder gibt es dafür schon eine Hilfsfunktion, die wiederverwendet werden soll?\n\n## Akzeptanzkriterien\nDie Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:\n1. `PoolChecker.CanAccessDocument`/`CanAccessFolder` fragen zusätzlich zur Eigentümerprüfung `ObjectAllowed` entlang des vollen Pfads ab; bei `hasEntries == false` gilt weiterhin nur die Eigentümerregel (keine Verhaltensänderung für Mandanten ohne Objektrechte)\n2. Bei `hasEntries == true` entscheidet ausschließlich das Ergebnis von `ObjectAllowed` (nicht zusätzlich die Eigentümerregel) — ein Objektrechte-Eintrag kann sowohl erweitern als auch einschränken\n3. `searchapi` filtert bei vorhandenen Objektrechten auf die Menge der laut `ObjectAllowed` erlaubten Ordner-IDs statt hart auf `created_by`\n4. Ausfall von Core/RBAC-11 während der Objektrechte-Abfrage führt zur Ablehnung (fail-closed), nie zu implizitem Zugriff\n5. Dokumentiert, wie `groupIDs` (Frage 1) tatsächlich befüllt werden, inklusive Entscheidung ob das eine Abhängigkeit zu einer weiteren Kachel ist\n\n## Prüfungen vor der Abnahme\nFühre jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:\n1. Mandant ohne jeden Objektrechte-Eintrag: Verhalten identisch zum Stand vor dieser Kachel (Regressionsnachweis)\n2. Dokument mit explizitem Freigabe-Eintrag für einen fremden Nutzer: dieser Nutzer kann nun lesen/schreiben (je nach `right`), vorher nicht\n3. Dokument mit explizitem Entzug für den Ersteller selbst: Ersteller wird abgelehnt, obwohl Eigentümer\n4. RBAC-11-Dienst nicht erreichbar während einer Objektrechte-Abfrage: Zugriff wird verweigert, nicht gewährt\n5. Suche liefert bei vorhandenen Objektrechten nur die tatsächlich erlaubten Treffer, nicht mehr/weniger\n\n## Nicht Bestandteil dieser Kachel\nDiese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:\n- `SEC-09` CMIS-Autorisierungs-Audit (verwandter, aber eigenständiger Befund)\n- UI zum Verwalten von Freigaben (reine Backend-Kachel hier)\n- Core-seitige Erweiterung des JWT um Gruppen-Claims, falls das die Antwort auf Frage 1 ist (separate Kachel im core-kanban)\n\n## Arbeitsweise\n1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.\n2. Kläre die beiden offenen Fragen oben, bevor du Akzeptanzkriterien als final behandelst.\n3. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.\n4. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.\n5. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.\n6. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.\n7. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.\n\n## Abschluss\n- Commit-Nachricht: `SEC-08: objektrechte-ordner-acl-verdrahtet`\n- Branch: `feature/sec-08-objektrechte-ordner-acl-verdrahtet`\n- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.\n- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten sowie die Antworten auf die offenen Fragen.\n" + }, + { + "code": "SEC-09", + "title": "CMIS-Autorisierungspfad geprueft: kein Bypass bestaetigt", + "epic": "SEC", + "featureIds": [], + "role": "Backend", + "complexity": "Mittel", + "estimateDays": 2, + "tech": "CMIS-Zugriffspruefung klaeren und absichern", + "description": "Vermuteter Bypass geprueft und per echtem Test (TestPermissionParityWithREST, Host 204, TEST_TENANT_DSN) widerlegt: CMIS-Backend ist auf die komplette Middleware-Kette verdrahtet, interner Re-Dispatch prueft CanAccessDocument/CanAccessFolder erneut. Kein Code-Fix noetig.", + "dependsOn": [], + "akzeptanz": [ + "Dokumentiert und per Test belegt, dass CMIS-Zugriff auf fremdes Dokument NICHT durchgeht - erledigt", + "Kein Code-Fix erforderlich, da kein Bypass bestaetigt", + "Verbleibendes AllowCaller-Restrisiko als bereits durch SEC-07 abgedeckt dokumentiert" + ], + "pruefungen": [ + "Ausgangsmessung durchgefuehrt: bestehender Test TestPermissionParityWithREST auf Host 204 gegen echte migrierte Testdatenbank ausgefuehrt, PASS", + "Fremder Nutzer mit gueltigem Token liest fremdes Dokument ueber CMIS: 403 bestaetigt", + "Gefaelschter caller-Parameter neben echtem fremden Token: 403 bestaetigt (Token-Identitaet gewinnt)", + "REST und CMIS liefern fuer alle Tokenzustaende identische Statuscodes" + ], + "status": "Fertig", + "epicName": "Sicherheit", + "epicEmoji": "🔒", + "wave": 13, + "unlocks": [], + "prompt": "# SEC-09 – CMIS-Autorisierungspfad geprüft: kein Bypass bestätigt\n\n**Epic:** 🔒 Sicherheit | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 2 Personentage | **Welle:** 13 | **Status:** Fertig (als Audit, kein Code-Fix nötig)\n\n---\n\n## Ausgangsverdacht\nVergleichsreview (Fable 5.1) vermutete zwei mögliche Lücken im CMIS-Browser-Binding:\n\n1. `internal/authz/authz.go` behandelt den Pfad `cmis` im Autorisierungs-Gate pauschal als erlaubt (`case \"cmis\": return true`), ohne `CanAccessDocument`/`CanAccessFolder` aufzurufen.\n2. `internal/cmis/backend.go` (`callerOf`) liest die Identität für die interne Weiterleitung aus `r.URL.Query().Get(\"caller\")` bzw. `X-User-Id`, statt die durch `moduleauth.Authenticate` verifizierte Identität zu verwenden.\n\n## Ergebnis der Prüfung\n**Kein Bypass.** Beide Punkte sind durch die Architektur bereits abgedeckt:\n\n1. `cmd/app/main.go:109` (`cmisBackend.Set(routes)`) verdrahtet das CMIS-Backend auf die **komplette** Middleware-Kette (`identityChain(guardMux(mux, ...))` = `moduleauth.Authenticate` → `authz.evaluate` → `mux`), nicht auf den rohen inneren Mux. Jede CMIS-Dokument-/Ordner-Anfrage wird intern in eine REST-Anfrage übersetzt (`cmis/backend.go newRequest`) und läuft dadurch **erneut** durch `CanAccessDocument`/`CanAccessFolder`. Der pauschale `\"cmis\": return true`-Fall im äußeren Gate betrifft nur die Zulässigkeit des CMIS-Einstiegspunkts selbst, nicht die Objektrechte — die werden eine Ebene tiefer korrekt geprüft.\n2. `newRequest` leitet den `Authorization`-Header der ursprünglichen Anfrage unverändert weiter (`backend.go:112`). Beim internen Re-Dispatch verifiziert `moduleauth.Authenticate` dieses Token erneut und überschreibt die Identität mit `claims.Subject` — ein im äußeren `callerOf` gelesener, potenziell manipulierter `?caller=`-Parameter wird dadurch verworfen, sobald ein gültiges Bearer-Token vorhanden ist.\n\n**Realer Test, nicht nur Code-Lektüre:** `dms/internal/cmis/cmis_test.go::TestPermissionParityWithREST` deckt exakt diese Szenarien bereits ab und wurde auf Host 204 gegen eine echte, migrierte Testdatenbank ausgeführt (`TEST_TENANT_DSN` gesetzt):\n\n```\n=== RUN TestPermissionParityWithREST\n--- PASS: TestPermissionParityWithREST (0.78s)\nPASS\n```\n\nIm Test bestätigt:\n- Fremder Nutzer mit gültigem eigenem Token liest fremdes Dokument über CMIS → `403` (Zeile „fremder Nutzer liest Dokument ueber CMIS“)\n- Gefälschter `?caller=`-Parameter zusätzlich zu einem echten, fremden Token → weiterhin `403` (Zeile „gefaelschter caller-Parameter neben fremdem Token“)\n- REST- und CMIS-Pfad liefern für alle Tokenzustände (Eigentümer, fremd, ohne Token, abgelaufen, falscher Mandant, manipuliert) identische Statuscodes\n\n## Verbleibendes, bewusst nicht behobenes Restrisiko\n`AllowCaller=true` (Demo-/Testmodus ohne jedes Bearer-Token) lässt weiterhin einen frei wählbaren `?caller=`-Wert zu — das betrifft REST und CMIS gleichermaßen und ist bereits als eigenständiges, allgemeines Risiko in `SEC-07` erfasst (nicht CMIS-spezifisch, kein neuer Befund).\n\n## Nicht Bestandteil dieser Kachel\n- `SEC-07` (`ALLOW_CALLER_IDENTITY` allgemein) — dort bereits behandelt\n- `SEC-08` (Objektrechte-Verdrahtung) — eigenständiger, unabhängiger Befund\n\n## Abschluss\n- Keine Code-Änderung nötig; Ticket dokumentiert eine durchgeführte, negative Sicherheitsprüfung.\n- Commit-Nachricht: `SEC-09: cmis-autorisierungspfad-geprueft-kein-bypass`\n- Branch: `feature/sec-09-cmis-autorisierungspfad-geprueft-kein-bypass`\n" } ] } diff --git a/dms-kanban/tickets/SEC-08.md b/dms-kanban/tickets/SEC-08.md new file mode 100644 index 00000000..84e104ba --- /dev/null +++ b/dms-kanban/tickets/SEC-08.md @@ -0,0 +1,66 @@ +# SEC-08 – Objektrechte (Ordner-ACL) existieren im Code, werden aber nie geprüft + +**Epic:** 🔒 Sicherheit | **Rolle:** Backend | **Komplexität:** Komplex | **Aufwand:** 3 Personentage | **Welle:** 13 + +--- + +## Rolle +Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH DMS (Dokumentenkern, Suche & OCR, Metadaten, Versionierung, Integration/API), das sich an paperless-ngx, Alfresco, Docspell und ecoDMS orientiert und bekannte Fehler aus archivdms (siehe known-issues-archivdms.md) bewusst vermeidet. +Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche. + +## Auftrag +Vergleichsreview gegen die vier Referenzprojekte (Fable 5.1) hat eine strukturelle Lücke gefunden, die paperless-ngx und Docspell ebenfalls haben und die NEXARCH laut `dms/README.md` bewusst vermeiden wollte: `internal/authz.PoolChecker` entscheidet Sichtbarkeit ausschließlich über `created_by == caller` (`internal/authz/authz.go:36-67`) plus eine statische Admin-Liste/-Rolle. `internal/policyclient.Client.ObjectAllowed` (`internal/policyclient/policyclient.go:104`) implementiert bereits eine Core-RBAC-11-Anfrage für Objektrechte entlang eines Ordner-/Dokumentpfads inklusive Gruppen (`groupIDs`) — wird aber an keiner Stelle im Produktionscode aufgerufen (`grep -r ObjectAllowed` außerhalb von `policyclient.go` selbst liefert nichts in `apiwiring`/`cmd`). Die Suche filtert ebenfalls hart auf den Ersteller (`internal/searchapi/handler.go:118`, `FilterCreatedBy`). Ergebnis: Freigabe eines Dokuments/Ordners an andere Nutzer oder Gruppen ist im DMS schlicht nicht möglich, trotz vorhandener Schnittstelle dafür. + +**Technische Grundlage:** `internal/authz/authz.go`, `internal/policyclient/policyclient.go` (`ObjectAllowed`, `ObjectPathNode`), `internal/searchapi/handler.go`, Migration 0006 (materialized path für Ordner) + +## Produkt-DNA für diesen Bereich +**Übernehmen:** +- Alfresco: echte Objekt-/Ordner-ACLs, Freigabe an Nutzer und Gruppen, nicht nur Ersteller. + +**Bewusst vermeiden:** +- paperless-ngx/Docspell: Sichtbarkeit faktisch nur über Eigentümerschaft, kein Teilen/Delegieren möglich. + +## Ausgangslage +`ObjectAllowed` existiert fertig implementiert inklusive `hasEntries`-Rückwärtskompatibilitätssignal (kein Objektrechte-Eintrag entlang des Pfads = wie bisher verhalten), ist aber nicht verdrahtet. Zwei offene Vorfragen, bevor Umsetzung sinnvoll ist (siehe unten). + +## Offene Fragen vor Umsetzungsbeginn +Diese müssen geklärt sein, bevor die Akzeptanzkriterien final feststehen — bei Unklarheit nachfragen, nicht raten: +1. **Herkunft von `groupIDs`:** `moduleauth.Identity` (`internal/moduleauth/middleware.go:12-15`) hat nur `UserID`, `Tenant`, `Role` — keine Gruppenmitgliedschaften. Muss das Modul-JWT um einen `groups`-Claim erweitert werden (Core-seitige Änderung, andere Kachel?), oder gibt es bereits eine andere Quelle für Gruppenzugehörigkeit? +2. **Pfadauflösung:** Für `ObjectAllowed` wird der volle Pfad von der Wurzel bis zum Objekt gebraucht (`[]ObjectPathNode`). Migration 0006 legt einen materialized path für Ordner an — reicht eine rekursive Abfrage über `folders.path`, oder gibt es dafür schon eine Hilfsfunktion, die wiederverwendet werden soll? + +## Akzeptanzkriterien +Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind: +1. `PoolChecker.CanAccessDocument`/`CanAccessFolder` fragen zusätzlich zur Eigentümerprüfung `ObjectAllowed` entlang des vollen Pfads ab; bei `hasEntries == false` gilt weiterhin nur die Eigentümerregel (keine Verhaltensänderung für Mandanten ohne Objektrechte) +2. Bei `hasEntries == true` entscheidet ausschließlich das Ergebnis von `ObjectAllowed` (nicht zusätzlich die Eigentümerregel) — ein Objektrechte-Eintrag kann sowohl erweitern als auch einschränken +3. `searchapi` filtert bei vorhandenen Objektrechten auf die Menge der laut `ObjectAllowed` erlaubten Ordner-IDs statt hart auf `created_by` +4. Ausfall von Core/RBAC-11 während der Objektrechte-Abfrage führt zur Ablehnung (fail-closed), nie zu implizitem Zugriff +5. Dokumentiert, wie `groupIDs` (Frage 1) tatsächlich befüllt werden, inklusive Entscheidung ob das eine Abhängigkeit zu einer weiteren Kachel ist + +## Prüfungen vor der Abnahme +Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen: +1. Mandant ohne jeden Objektrechte-Eintrag: Verhalten identisch zum Stand vor dieser Kachel (Regressionsnachweis) +2. Dokument mit explizitem Freigabe-Eintrag für einen fremden Nutzer: dieser Nutzer kann nun lesen/schreiben (je nach `right`), vorher nicht +3. Dokument mit explizitem Entzug für den Ersteller selbst: Ersteller wird abgelehnt, obwohl Eigentümer +4. RBAC-11-Dienst nicht erreichbar während einer Objektrechte-Abfrage: Zugriff wird verweigert, nicht gewährt +5. Suche liefert bei vorhandenen Objektrechten nur die tatsächlich erlaubten Treffer, nicht mehr/weniger + +## Nicht Bestandteil dieser Kachel +Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor: +- `SEC-09` CMIS-Autorisierungs-Audit (verwandter, aber eigenständiger Befund) +- UI zum Verwalten von Freigaben (reine Backend-Kachel hier) +- Core-seitige Erweiterung des JWT um Gruppen-Claims, falls das die Antwort auf Frage 1 ist (separate Kachel im core-kanban) + +## Arbeitsweise +1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster. +2. Kläre die beiden offenen Fragen oben, bevor du Akzeptanzkriterien als final behandelst. +3. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche. +4. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test. +5. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis. +6. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code. +7. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen. + +## Abschluss +- Commit-Nachricht: `SEC-08: objektrechte-ordner-acl-verdrahtet` +- Branch: `feature/sec-08-objektrechte-ordner-acl-verdrahtet` +- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy. +- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten sowie die Antworten auf die offenen Fragen. diff --git a/dms-kanban/tickets/SEC-09.md b/dms-kanban/tickets/SEC-09.md new file mode 100644 index 00000000..c19d177f --- /dev/null +++ b/dms-kanban/tickets/SEC-09.md @@ -0,0 +1,42 @@ +# SEC-09 – CMIS-Autorisierungspfad geprüft: kein Bypass bestätigt + +**Epic:** 🔒 Sicherheit | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 2 Personentage | **Welle:** 13 | **Status:** Fertig (als Audit, kein Code-Fix nötig) + +--- + +## Ausgangsverdacht +Vergleichsreview (Fable 5.1) vermutete zwei mögliche Lücken im CMIS-Browser-Binding: + +1. `internal/authz/authz.go` behandelt den Pfad `cmis` im Autorisierungs-Gate pauschal als erlaubt (`case "cmis": return true`), ohne `CanAccessDocument`/`CanAccessFolder` aufzurufen. +2. `internal/cmis/backend.go` (`callerOf`) liest die Identität für die interne Weiterleitung aus `r.URL.Query().Get("caller")` bzw. `X-User-Id`, statt die durch `moduleauth.Authenticate` verifizierte Identität zu verwenden. + +## Ergebnis der Prüfung +**Kein Bypass.** Beide Punkte sind durch die Architektur bereits abgedeckt: + +1. `cmd/app/main.go:109` (`cmisBackend.Set(routes)`) verdrahtet das CMIS-Backend auf die **komplette** Middleware-Kette (`identityChain(guardMux(mux, ...))` = `moduleauth.Authenticate` → `authz.evaluate` → `mux`), nicht auf den rohen inneren Mux. Jede CMIS-Dokument-/Ordner-Anfrage wird intern in eine REST-Anfrage übersetzt (`cmis/backend.go newRequest`) und läuft dadurch **erneut** durch `CanAccessDocument`/`CanAccessFolder`. Der pauschale `"cmis": return true`-Fall im äußeren Gate betrifft nur die Zulässigkeit des CMIS-Einstiegspunkts selbst, nicht die Objektrechte — die werden eine Ebene tiefer korrekt geprüft. +2. `newRequest` leitet den `Authorization`-Header der ursprünglichen Anfrage unverändert weiter (`backend.go:112`). Beim internen Re-Dispatch verifiziert `moduleauth.Authenticate` dieses Token erneut und überschreibt die Identität mit `claims.Subject` — ein im äußeren `callerOf` gelesener, potenziell manipulierter `?caller=`-Parameter wird dadurch verworfen, sobald ein gültiges Bearer-Token vorhanden ist. + +**Realer Test, nicht nur Code-Lektüre:** `dms/internal/cmis/cmis_test.go::TestPermissionParityWithREST` deckt exakt diese Szenarien bereits ab und wurde auf Host 204 gegen eine echte, migrierte Testdatenbank ausgeführt (`TEST_TENANT_DSN` gesetzt): + +``` +=== RUN TestPermissionParityWithREST +--- PASS: TestPermissionParityWithREST (0.78s) +PASS +``` + +Im Test bestätigt: +- Fremder Nutzer mit gültigem eigenem Token liest fremdes Dokument über CMIS → `403` (Zeile „fremder Nutzer liest Dokument ueber CMIS“) +- Gefälschter `?caller=`-Parameter zusätzlich zu einem echten, fremden Token → weiterhin `403` (Zeile „gefaelschter caller-Parameter neben fremdem Token“) +- REST- und CMIS-Pfad liefern für alle Tokenzustände (Eigentümer, fremd, ohne Token, abgelaufen, falscher Mandant, manipuliert) identische Statuscodes + +## Verbleibendes, bewusst nicht behobenes Restrisiko +`AllowCaller=true` (Demo-/Testmodus ohne jedes Bearer-Token) lässt weiterhin einen frei wählbaren `?caller=`-Wert zu — das betrifft REST und CMIS gleichermaßen und ist bereits als eigenständiges, allgemeines Risiko in `SEC-07` erfasst (nicht CMIS-spezifisch, kein neuer Befund). + +## Nicht Bestandteil dieser Kachel +- `SEC-07` (`ALLOW_CALLER_IDENTITY` allgemein) — dort bereits behandelt +- `SEC-08` (Objektrechte-Verdrahtung) — eigenständiger, unabhängiger Befund + +## Abschluss +- Keine Code-Änderung nötig; Ticket dokumentiert eine durchgeführte, negative Sicherheitsprüfung. +- Commit-Nachricht: `SEC-09: cmis-autorisierungspfad-geprueft-kein-bypass` +- Branch: `feature/sec-09-cmis-autorisierungspfad-geprueft-kein-bypass`