diff --git a/.planning/todos/pending/2026-08-11-ldap-bindpasswort-unverschluesselt.md b/.planning/todos/pending/2026-08-11-ldap-bindpasswort-unverschluesselt.md new file mode 100644 index 0000000..ea45569 --- /dev/null +++ b/.planning/todos/pending/2026-08-11-ldap-bindpasswort-unverschluesselt.md @@ -0,0 +1,59 @@ +--- +created: 2026-08-11 +title: LDAP-Bind-Passwort liegt im Klartext in der DB, obwohl der Verschluesselungsdienst schon existiert +area: ldap +severity: major +trigger: vor dem Verkauf an externe Kunden; unabhaengig davon jederzeit machbar, da klein und ohne Produktfragen +files: + - apps/api/prisma/schema.prisma:72 + - apps/api/src/ldap/ldap-config.service.ts:38 + - apps/api/src/calendar/crypto.service.ts +--- + +## Problem + +`LdapConfig.bindPassword` wird so gespeichert, wie es aus dem Formular kommt — +`ldap-config.service.ts:38` und `:76` schreiben den Wert unveraendert in die +Datenbank. Wer Lesezugriff auf die Datenbank hat, hat damit die Zugangsdaten des +AD-Dienstkontos. + +Hashen ist hier KEIN gangbarer Weg: Tessera muss sich mit diesem Passwort am +Domain Controller anmelden, braucht es also im Original. Der richtige Weg ist +symmetrische Verschluesselung mit einem Schluessel ausserhalb der Datenbank. + +Genau das macht das Projekt an drei anderen Stellen bereits, mit AES-256-GCM: + +| Feld | Modell | +|---|---| +| `encryptedPassword` | `CalendarSource` (schema.prisma:333) | +| `encryptedInboxCreds` | `DkvModuleConfig` (:264) | +| `encryptedInboxCreds` | `TenderEmailConfig` (:289) | +| `encryptedPassword` | `SmtpConfig`-naher Bereich (:237) | + +Der Dienst dafuer existiert: `calendar/crypto.service.ts`, AES-256-GCM, Schluessel +aus der Umgebungsvariable `CALENDAR_ENCRYPTION_KEY`. Die LDAP-Konfiguration nutzt +ihn schlicht nicht — LDAP war frueher dran als der Verschluesselungsdienst und +wurde nie nachgezogen. + +Was bereits richtig ist: die API gibt das Passwort nie heraus, `ldap.controller.ts` +maskiert es an allen vier Stellen zu `********`. + +Aufgefallen am 2026-08-11 waehrend der Phase-16-Tests, als die LDAP-Konfiguration +fuer eine read-only-Pruefung aus der Datenbank gelesen wurde. + +## Solution + +1. Den bestehenden Verschluesselungsdienst aus `calendar/` heraushebeln, damit er + nicht laenger nach einem Modul benannt ist, das ihn zufaellig zuerst brauchte — + samt Schluesselvariable. Alternativ zunaechst unveraendert wiederverwenden und + die Umbenennung als eigenen Schritt fuehren; das ist die kleinere Aenderung. +2. `LdapConfigService.create/update` verschluesselt schreiben, alle Lesestellen + entschluesseln. Lesestellen sind `ldap.controller.ts` (Zeilen 136, 177, 210, + 244, 277, 313) und der Sync selbst. +3. Migration, die den vorhandenen Klartext-Eintrag einmalig verschluesselt. + Muster vorhanden — dieselbe Form wie die bestehenden Backfill-Migrationen. +4. Feld umbenennen (`bindPassword` → `encryptedBindPassword`), damit am Schema + ablesbar ist, was drinsteht, wie bei den anderen vier Feldern. + +Keine offenen Produktfragen — reine Umsetzung, entlang eines im Projekt bereits +etablierten Musters. diff --git a/.planning/todos/pending/2026-08-11-modulaktivierung-ohne-lizenzpruefung.md b/.planning/todos/pending/2026-08-11-modulaktivierung-ohne-lizenzpruefung.md new file mode 100644 index 0000000..d3af1c3 --- /dev/null +++ b/.planning/todos/pending/2026-08-11-modulaktivierung-ohne-lizenzpruefung.md @@ -0,0 +1,60 @@ +--- +created: 2026-08-11 +title: Jeder Mandanten-Admin kann sich jedes Modul selbst freischalten — Aktivierung ohne Lizenzpruefung +area: module-registry +severity: major +trigger: vor dem Verkauf an externe Kunden — intern folgenlos, extern haelt die Modullizenzierung nicht +files: + - apps/api/src/module-registry/module-registry.service.ts:53 + - apps/api/src/module-registry/module-registry.controller.ts:103 + - apps/api/prisma/schema.prisma:97 +--- + +## Problem + +`POST /modules/:moduleId/activate` ist nur mit `@Roles(ADMIN, SUPER_ADMIN)` +geschuetzt. Die `tenantId` stammt aus dem Login des Aufrufers, betrifft also +immer den eigenen Mandanten — insoweit ist die Mandantentrennung intakt. + +`activateForTenant()` (`module-registry.service.ts:53`) prueft danach nur noch, +ob das Modul im Katalog existiert, und legt dann die `TenantModuleActivation` an. +Es gibt keine Pruefung, ob der Mandant dieses Modul erworben hat. + +Im Datenmodell existiert dafuer auch nichts: es gibt `Module` (Katalog) und +`TenantModuleActivation` (an/aus pro Mandant), aber kein Objekt, das eine +Lizenz, ein Kontingent oder eine Laufzeit abbildet. **"Aktiviert" und +"lizenziert" sind heute dasselbe.** + +Ein Kunden-Administrator kann sich damit unter Admin → Module jedes Modul aus +dem Katalog selbst freischalten. + +Nicht betroffen ist die Freigaben-Matrix — die verteilt nur innerhalb des +bereits Aktivierten: `getMatrix()` (`module-grants.service.ts:174`) listet +ausschliesslich Module mit `isActive = true`, und `grant()` (`:93`) weist ein +nicht aktiviertes Modul mit einer Fehlermeldung ab. Die Luecke sitzt eine Ebene +darueber, in der Aktivierung selbst. + +Aufgefallen am 2026-08-11, als der User beim Blick auf die Freigaben-Matrix +fragte, ob Mandantenfaehigkeit und Modullizenzierung so ueberhaupt tragen. + +## Solution + +Noch offen — als Entwurf, nicht als Entscheidung: + +Lizenzierung von Aktivierung trennen. Ein eigenes Objekt (etwa +`TenantModuleLicense`) haelt fest, welche Module ein Mandant erworben hat, mit +Laufzeit und ggf. Nutzerzahl. `activateForTenant()` prueft dagegen und weist ab, +was nicht lizenziert ist — analog zu der Pruefung, die `grant()` bereits gegen +die Aktivierung macht. + +Zu klaeren, bevor gebaut wird — das sind Produktfragen fuer den User: + +- Wer vergibt Lizenzen? Nur der Plattform-Betreiber (SUPER_ADMIN eines + Betreiber-Mandanten), oder gibt es eine Selbstbedienung ueber den Marktplatz? +- Was passiert mit einer ablaufenden Lizenz — sofort abschalten, Schonfrist, + nur noch lesend? +- Braucht es ueberhaupt Laufzeiten, oder reicht "gekauft / nicht gekauft"? +- Wie kommt eine Lizenz technisch in eine Kundeninstallation, die beim Kunden + im eigenen Netz laeuft? (Lizenzdatei, Schluessel, Online-Abgleich?) + +Solange Tessera nur intern laeuft, ist der Ist-Zustand folgenlos.