Der Kernfund dieser Etappe war ein Defekt im Fundament: forTenant() setzte den
Mandantenkontext auf der Transaktionsverbindung, dispatchte die Abfrage aber
ueber den aeusseren Client. Empirisch reproduziert statt hergeleitet —
set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL.
Damit hat die Mandantentrennung nie funktioniert, auch nicht dort, wo sie
scheinbar benutzt wurde. Nach einem Scharfschalten haette sich das umgekehrt:
die betroffenen Abfragen liefern dann null Zeilen, und der LDAP-Loeschzweig
haette das als "Gruppe im Verzeichnis verschwunden" gedeutet und sie samt
Mitgliedschaften und Modulfreigaben geloescht. Aufgefallen, weil vor dem
Umbau geprueft wurde, ob das Fundament traegt.
Der Anmeldeweg bekam eine bewusst schmale Ausnahme: drei SECURITY-DEFINER-
Funktionen mit fester Spaltenliste, Gleichheitsbedingung und LIMIT 1. Eine
Policy waere hier untauglich — sie ist ein Zeilenpraedikat und haette
zwangslaeufig die ganze Tabelle freigegeben.
Die Klassifikation macht die restliche Arbeit planbar: 227 Zugriffe in 59
Einheiten, davon 31 umzustellen, 9 teilweise, 16 ohne Mandantenbezug und 3
bewusst uebergreifend. Ein Test haelt die Einteilung gegen Abdriften fest.
Browser-Gegenprobe lokal bestanden. 701 Tests gruen. Der Schalter bleibt aus;
#18, #19 und #20 bleiben offen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
Beim Nachtragen kam heraus, dass auf dem Server gar nicht docker-compose.yml
gilt: die .env setzt COMPOSE_FILE=docker-compose.prod.yml. Meine erste Angabe
nannte deshalb die falsche Datei — aufgefallen, weil nach dem Eintragen
'docker compose config' weiterhin nur pgdata rendern wollte.
Die drei Zeilen sind jetzt in docker-compose.prod.yml ergaenzt, Sicherung
liegt als docker-compose.prod.yml.bak.20260909-0818 daneben.
Nach dem Neuerstellen durch den User belegt statt behauptet:
- Mount tessera_user-files -> /app/user-files am laufenden Container
- der Ordner gehoert uid 1001, das benannte Volume hat die Eigentuemerschaft
beim ersten Einhaengen uebernommen (genau der Grund, warum es kein
Bind-Mount wurde)
- Schreiben als Dienstnutzer funktioniert
- eine Probedatei lag unter /var/lib/docker/volumes/tessera_user-files/_data
auf dem Host, also ausserhalb des Containers; danach wieder entfernt
Ledger: 16 behoben, 1 zurueckgestellt, offen nur noch #18 und #19.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
Die Arbeit liefert Rolle, Verbindungstrennung, Pruefwerkzeug und Policies fuer
alle 16 offenen Tabellen, schaltet die Trennung aber bewusst NICHT scharf.
Grund, gemessen statt vermutet: im Code stehen 182 Datenbankzugriffe ohne
Mandantenkontext gegen 19 mit. Der Anmeldeweg ist zwingend darunter — er liest
die Benutzerzeile, bevor der Mandant bekannt ist, weil der Mandant erst aus
dieser Zeile kommt. Unter einer Rolle ohne Umgehungsrecht liefert diese Abfrage
nichts, und niemand koennte sich mehr anmelden. Das Scharfschalten ist damit
ein eigener Vorgang, kein Nebeneffekt dieser Arbeit.
Neu im Ledger als #19: SearchProvider und TenderRssFeedSource haben ein
nullable tenantId. Die einfache Policy vergleicht NULL nie gleich, wodurch die
plattformweiten Zeilen nach dem Scharfschalten fuer JEDEN Mandanten
verschwinden wuerden — nicht nur fuer fremde. Heute wirkungslos, beim
Scharfschalten zwingend mitzuloesen. Die richtige Semantik ist eine
Produktentscheidung, deshalb bewusst nicht eigenmaechtig anders geloest.
673/673 Tests gruen, Typpruefung sauber. #18 bleibt offen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
Zwei kleine, unabhaengige Reparaturen in getrennten Commits (dab72eb, c807049).
Bemerkenswert an der Versionskorrektur: sie hat sechs Empfehlungen zutage
gefoerdert, die nie eingebaut wurden — Keycloak als Identitaetsanbieter, Redis,
TanStack Query, shadcn/ui, Playwright als Projektabhaengigkeit und Husky. Die
stehen jetzt in einem eigenen Abschnitt 'Recommended But Not Adopted', damit
niemand sie beim Lesen fuer vorhanden haelt. Ausserdem laeuft Vitest in den
beiden Anwendungen in unterschiedlichen Hauptfassungen (3.2.6 gegen 4.1.9).
Der Technik-Block in CLAUDE.md ist generiert. Eine Korrektur allein dort waere
bei der naechsten Regeneration still zurueckgeholt worden, deshalb zusaetzlich
ein Herkunftsvermerk im Block und eine datierte Hinweiszeile in der
Recherchedatei, deren Zahlen unveraendert bleiben.
Nebenbei zwei Verfaelschungen in STATE.md zurueckgesetzt, die Werkzeugaufrufe
hinterlassen hatten: eine Platzhalterzeile in der Quick-Task-Tabelle und
verfaelschte Fortschrittszahlen (3/82 statt 17/83).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
Auf Entscheidung des Users vom 2026-09-09. Der Wunsch bleibt bestehen, hat aber
keine Dringlichkeit und wird nicht mehr als naechster Bau-Kandidat vorgelegt —
erst wieder aufgreifen, wenn der User ihn von sich aus nennt.
Damit ist derzeit kein naechster Schritt vorgemerkt: das Ledger ist leer, und
alle verbliebenen Punkte ruhen bewusst (Postfach-Test mangels Postfach,
Mandantentrennung solange Tessera intern laeuft, Lizenzpruefung bis alle Module
intern laufen, Branding ab jetzt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
Abnahme im Browser auf alpha gegen das echte AD, nachdem der User die Container
neu erstellt hatte. Voraussetzungen gemessen statt angenommen: Container
tatsaechlich getauscht, Migration 20260909120000_user_email_optional angewandt,
User.email is_nullable = YES. Die Migration lief beim API-Start automatisch mit.
#15 — der Sync meldet jetzt "Erstellt: 4" statt vier stiller Fehlschlaege. Die
kollidierenden Konten sind angelegt und aktiv, ohne Adresse; mbuntz behaelt als
erster Anspruch seine. Der Bericht zeigt zwei verstaendliche deutsche
Abschnitte statt Prisma-Text und englischer Meldungen. Die beiden Folgestellen
vertragen Konten ohne Adresse: die Benutzerliste zeigt einen Gedankenstrich,
und die Suche im Mitglieder-Dialog liefert die Konten sauber statt
abzustuerzen — das war der eigentliche Fallstrick.
#14 — die Matrix-Suche laesst die nicht getroffene Achse stehen. Alle vier
Faelle geprueft: Modulname behaelt die Gruppenspalten, Gruppenname behaelt die
Modulzeile, der AD-Name findet dieselbe Spalte wie der interne Name
(Regression #6c intakt), und eine Suche ohne Treffer erklaert sich in einem
verstaendlichen Satz.
Am Active Directory wurde nichts veraendert; es wurde nur gelesen. Der fuer die
Regressionsprobe gesetzte interne Name ist wieder geleert.
Ledger danach: 0 offen, 15 behoben, 1 zurueckgestellt (#12 — es gibt intern
kein Postfach fuer Ausschreibungs-Alarme).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
Verifikation unabhaengig nachgemessen: 651/651 API-Tests, 233/233 Web-Tests,
beide Typpruefungen sauber.
Der Sicherheitsfund T-Q3-01 wurde nicht nur gruen getestet, sondern
falsifiziert: der Verifizierer hat die neue Besitzpruefung testweise
zurueckgebaut, woraufhin der Test fehlschlug und die fremde Adresse
tatsaechlich in prisma.user.update() landete. Danach sauber zurueckgesetzt.
Ebenso gegengeprueft: beide Anlege-Wege (Sync und Einzelimport) nutzen
denselben Kollisionsentscheider, im ausgelieferten Code stehen keine
kundenspezifischen Namen oder Adressen, rohe ORM-Texte erreichen die
Oberflaeche nicht mehr, und die Gruppensuche unter internem wie AD-Namen
(#6c) ist per Regressionstest gesichert.
Eine Abweichung des Ausfuehrenden ist dokumentiert und bestaetigt: die im
Plan vorgesehene Testvorlage war wegen Kurzschlussauswertung schon gegen den
unveraenderten Code gruen; mit einem zweiten, nicht passenden Konto
reproduziert sie den Absturz nun wirklich.
Status human_needed: die fuenf Browser-Pruefungen brauchen einen Neubau und
sind Sache des Users. WINDOWS #14 und #15 bleiben bis dahin offen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
Abnahme im Browser auf alpha gegen den echten Exchange owa.ctl.de, nachdem die
Container mit --force-recreate getauscht waren. Der blosse pull genuegte nicht:
die neue Route fehlte im laufenden Container und war im gezogenen Image
vorhanden — hart belegt statt vermutet.
Alle vier Punkte bestanden:
- gespeicherte Konfiguration mit LEEREM Passwortfeld meldet "Verbindung
erfolgreich" — belegt zugleich den Rueckgriff auf die gespeicherten
verschluesselten Zugangsdaten
- absichtlich falscher Server meldet "Verbindung fehlgeschlagen: getaddrinfo
ENOTFOUND owa-gibtsnicht.ctl.de"
- keine Zugangsdaten im API-Protokoll (die Treffer einer Mustersuche waren
Routennamen wie /auth/reset-password)
- der Test schreibt nichts; die gespeicherte Konfiguration blieb unveraendert
Nebenbefund mit Gewicht: das Protokoll zeigt eine echte Antwort von Exchange
2019 (ServerVersionInfo 15.2) samt aufgeloester FolderId. Der in Plan 14-03 als
fragil markierte, handgeschriebene NTLM/SOAP-Weg ist damit erstmals gegen einen
echten Server gemessen — bisher lag nur gemocktes httpntlm.post vor.
WINDOWS #12 zurueckgestellt (waived): es gibt intern kein Postfach, in das
Ausschreibungs-Alarme hereinkommen. Ein frueheres Missverstaendnis hatte eines
angenommen. Nachzuholen, sobald ein solches Postfach existiert; offen ist dann
allein das Einlesen einer echten Alarm-Mail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU