docs: add two backlog items found during Phase 16 testing
Neither is a Phase 16 defect; both surfaced while testing it and would otherwise have been lost with the session. 1. Module activation has no licence check — a tenant admin can activate any catalogue module for their own tenant. The grants matrix is NOT the hole: it only distributes what is already active. Carries open product questions (who issues licences, what expiry does), so it is written up as a draft, not a decision. 2. The LDAP bind password is stored in clear text although an AES-256-GCM service already exists and is used for calendar, DKV and tender inbox credentials. Hashing is not an option here — the password must be replayable to bind against the directory — so encryption at rest is the fix. No open questions, just work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user