Commit Graph

19 Commits

Author SHA1 Message Date
schalli 3d645674f0 feat(quick-260914-eym): forSystem(), is_system_context(), Systemleseregel auf fuenf Tabellen, DKV-Planer je Mandant — ein Pfad (WINDOWS #21)
- Helfer forSystem(prisma) in prisma-tenant.extension.ts (Array-Form,
  setzt app.system_context='true' und die beiden anderen Variablen
  ausdruecklich leer); forTenant()/withTenantTransaction() setzen
  app.system_context='' als Literal (4 neue Spec-Tests)
- Migration 20260914120000_rls_system_context_read: is_system_context()
  (COALESCE, STABLE) und system_read_policy FOR SELECT auf DkvModuleConfig,
  LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch — lokal
  angewendet (36 Migrationen, pg_proc 1, 5 system_read_policy, 34 Regeln)
- migration-sql.spec.ts: describe-Block fuer die neue Migration (6 Tests)
- rls-scratch-check.mjs: Funktion aus der Migration geschnitten,
  forSystemQuery/buildInlineSystemClient, Reset in forTenantQuery/
  buildInlineExtendedClient, runSystemContextChecks (4 Funktionsfaelle +
  9 Kennungen DkvModuleConfig) -> Alle 216 Pruefungen bestanden
- rls-access-inventory.spec.ts: fuenfte Erkennungsform const X = forSystem(,
  Stand system-gebunden mit Vorrangregel, FORSYSTEM_ALLOWED_CALL_SITES
  (exakte Zahl je Datei, 3 Tests), Proben C/D/E
- DKV: loadActiveConfigsForScheduler() ueber forSystem (findMany isActive,
  CONFIG_SAFE_SELECT, orderBy tenantId); DkvSchedulerService mit Auftrag je
  Mandant dkv-inbox-poll:<tenantId>, activeTenantId ersatzlos entfernt,
  setInterval/stopJob je Mandant, registeredTenantIds(); Controller
  stopJob(tenantId); neue dkv-scheduler.service.spec.ts (7 Tests),
  dkv.service.spec.ts Tests 6/7 umgestellt
- Klassifikation: dkv.service.ts/dkvModuleConfig system-gebunden, Header
  mit fuenfter Erkennungsform und viertem Stand-Wert
- Baseline: 63 Dateien / 1051 Tests, tsc 0, Werkzeug 216

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-14 11:32:54 +02:00
schalli f0b531b712 feat(quick-260911-nke): current_user_id(), Benutzerdimension in den Regeln, forTenant() mit userId — ein Pfad
- Neue Migration 20260911120000_rls_user_dimension_personal_tables: current_user_id()
  (NULLIF-gefaltet), zehn persoenliche Tabellen umgestellt (acht als eine Regel,
  SearchProvider/TenderRssFeedSource als je vier befehlsgetrennte Regeln), vier
  Verwaltungstabellen bewusst unveraendert. Lokal angewendet (migrate deploy,
  Prisma-Binary aus apps/api/node_modules/.bin), schema.prisma unveraendert.
- forTenant(prisma, tenantId, userId?): beide set_config in EINER getaggten
  Anweisung, $transaction-Array bleibt bei zwei Eintraegen (WINDOWS #20),
  Leerstring ohne Benutzer statt Weglassen.
- tender-saved-search.service.ts: alle vier forTenant()-Aufrufe reichen userId
  durch; Detektor-Regex bestaetigt 4 Treffer.
- rls-scratch-check.mjs: current_user_id() aus der neuen Migration geschnitten
  (nicht getippt), drei Funktionsfaelle gemessen, neue runUserDimensionChecks()
  mit generiertem Client fuer TenderSavedSearch (vier Wahrheiten + Spaltenabgleich),
  die alte Loch-Pruefung tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar
  umgedreht (alte Messung unter neuem Namen erhalten, neue Umkehrung MIT Benutzer).
  sqlStateOf() um Message-Fallback ergaenzt (RLS-Ablehnung ueber generierten
  Client traegt den SQLSTATE nur im Fehlertext, nicht in .meta.code).
- Baseline: 1020/62 Tests, Typpruefung sauber, Werkzeug 146/146 bestanden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-11 17:25:45 +02:00
schalli 6b237351e9 feat(quick-260910-jab): listForUser binden, die vier Aufzeichnungen im Quelltext richtigstellen
- TenderRssFeedSourceService.listForUser() nimmt jetzt (userId, tenantId)
  entgegen und laeuft ueber einen gebundenen Klienten (forTenant) — die neue
  Leseregel schliesst plattformweite Zeilen ein, die Reparatur haette den
  ungebundenen Pfad sonst still auf nur die plattformweiten Zeilen reduziert
  (Befund F). createPlatform/remove bleiben bewusst ungebunden, Kommentare an
  der neuen Regel richtiggestellt.
- TendersController.listRssFeeds reicht die Mandantenkennung aus dem
  Aufrufzusammenhang durch.
- Vier Aufzeichnungen im Quelltext (module-access.service.ts,
  groups.service.ts, module-grants.service.ts, rls-coverage.spec.ts) sagen
  jetzt, dass die Datenbankregel seit 20260910120000_rls_widen_membership_
  grant_and_platform_read beide Seiten prueft; die Anwendungspruefungen
  bleiben unveraendert bestehen (zweites Netz, wirkt vor dem Scharfschalten
  als einziger Schutz).
- Zwei-Klienten-Nachweis in module-grants.service.spec.ts ergaenzt (Kommentar,
  warum die beiden Cross-Tenant-Tests nach der Regelaenderung nicht entfallen
  duerfen) und in tender-rss-feed.service.spec.ts umgekehrt (listForUser
  bindet jetzt).
- Rule 1: implizites any beim Destrukturieren in listRssFeeds (feeds ist seit
  der Bindung `any`) mit expliziter Annotation behoben.
- Falsifizierungsnachweis durchgefuehrt: Bindungsaufruf zurueckgenommen,
  genau ein Test wurde rot (AssertionError, 0 statt der erwarteten
  Aufrufe), Ruecknahme rueckgaengig gemacht.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-10 14:35:13 +02:00
schalli f4f3115d5a feat(quick-260910-jab): drei zu kurz greifende RLS-Regeln schliessen (T-JTS-02, T-JTS-03, WINDOWS #19)
- Neue, handgeschriebene Migration 20260910120000_rls_widen_membership_grant_and_platform_read:
  GroupMembership prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer),
  ModuleGrant prueft zusaetzlich die referenzierte Gruppe/den referenzierten Benutzer
  (mit Leer-Zulassung, D-04), TenderRssFeedSource bekommt vier nach Befehl getrennte
  Regeln statt einer (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt
  weiterhin einen Mandanten). SearchProvider bewusst unveraendert (Befund E: Praemisse
  widerlegt). Lokal angewandt und gegen den Systemkatalog der lebenden Datenbank
  gemessen. Der Schalter bleibt aus (Rolle tessera).
- rls-scratch-check.mjs: die drei loch-behauptenden Pruefungen umgekehrt (nicht
  geloescht), Gegenmessungen ueber die Wartungsrolle ergaenzt, vier Befehlsrichtungen
  fuer TenderRssFeedSource gemessen, neuer Abschnitt fuer SearchProvider, Extraktion
  auf die neue Migration umgeleitet und um eine mehrfach-treffer-faehige Form ergaenzt
  (extractAllPolicySql).
- migration-sql.spec.ts: neuer Beschreibungsblock fuer die neue Migration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-10 14:29:25 +02:00
schalli abb6c8bea3 feat(jts-03): module-grants.service.ts binden, beide Dokumente schliessen
Alle fuenf Methoden von ModuleGrantsService (assertTargetBelongsToTenant,
grant, revoke, getMatrix, getUserAccess) laufen jetzt ueber forTenant(); bei
den beiden Datenlieferungen teilen sich alle parallel abgesetzten
Teilabfragen denselben gebundenen Client. Die Mandanten-Gegenpruefung vor
jedem Erteilen bleibt ausdruecklich bestehen und bekommt einen Verweis auf
Befund F/T-JTS-03: die Regel auf ModuleGrant prueft nur die
Mandantenkennung der Zeile, nicht die referenzierte Gruppe. Der veraltete
Kommentar ueber der Mitgliedschaftsabfrage im Benutzer-Detail ("kein
forTenant hier") ist durch den neuen Stand ersetzt.

module-grants.service.spec.ts bekommt denselben Bindungsnachweis-Mock wie
groups.service.spec.ts (zwei unterscheidbare Clients ueber demselben
Speicher) und sechs neue Bindungsnachweise; alle 28 Bestandstests bleiben
gruen.

Beide Dokumente geschlossen: die Bereichsuebersicht fuer groups ist neu
gemessen (0 ungebunden, 31 gebunden — ein dokumentierter methodischer
Bodensatz, da die einfache Rohtrefferzaehlung die neun ueber `tx` gebundenen
Zugriffe innerhalb der drei Transaktionen nicht sieht), die
Klassen-Verteilung auf 62 Paare aktualisiert, und der als offen gefuehrte
Befund D aus dem ldap-Abschnitt der Fehlerrichtung ist mit Verweis auf
diesen Durchlauf als erledigt vermerkt (Nachtrag, nicht Neuschrieb). 743
Tests und die Typpruefung gruen; Schema, Migrationen und alle vier
Compose-Dateien unveraendert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 15:07:09 +02:00
schalli 7f08b27eea feat(jts-02): groups.service.ts binden, Transaktionen tragfaehig machen, Absicherung sehend machen
prisma-tenant.extension.ts bekommt withTenantTransaction(prisma, tenantId, fn)
— die interaktive Transaktion auf dem UNgebundenen Client mit set_config als
erster Anweisung direkt auf tx, die in Aufgabe 1 als einzige der drei
gemessenen Formen sowohl die Einzelmessung als auch eine Lastprobe unter
echter Nebenlaeufigkeit bestand (die Array-Form auf dem gebundenen Client
verteilt jede Operation auf eine eigene Teiltransaktion; die interaktive Form
auf dem gebundenen Client brach unter 40 parallelen Aufrufen mit P2028 ab).

rls-access-inventory.spec.ts bekommt eine dritte Erkennung fuer
Modellzugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion
(zwei Formen: direkter Empfaenger.$transaction(async...) und das neue
Hilfsmittel withTenantTransaction(...)) — macht das Paar
(groups.service.ts, tenantModuleActivation) erstmals sichtbar, das bislang
keine Pruefung dieses Projekts je gesehen hat.

groups.service.ts: alle zwoelf Methoden inklusive der drei Transaktionen
(update() isDefault:true, reassignDefaultBeforeDelete(), ensureDefaultGroup())
laufen jetzt ueber den Mandantenkontext. Zaehler und Transaktion in
ensureDefaultGroup() sind gemeinsam gebunden (T-JTS-05). addUserToDefaultGroup()
prueft neu, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02) —
die Regel auf GroupMembership prueft nachweislich nur die Gruppenseite.

groups.service.spec.ts bekommt zwei unterscheidbare Clients ueber demselben
Speicher-Fake (Muster aus 260909-ipc, auf die interaktive Form uebertragen)
und 13 neue Bindungsnachweise; alle 42 Bestandstests bleiben gruen.
Klassifikationsdokument nachgezogen. 737 Tests und die Typpruefung gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 15:01:24 +02:00
schalli dd59bf592f fix(16): WR-01 name lock in GroupsService.update() also checks ldapDn
A legacy binding from plan 15-06 has ldapDn set but ldapObjectGuid stays
null until the first syncBoundGroupsForTenant() run backfills it. Until
then, GroupsService.update() let a direct PATCH rename through even
though GroupFormModal.tsx already treats the same group as AD-bound
(isImported = ldapDn != null) — the D-03 name lock was only a UI
convention for that window, not the backend invariant the 16-02 summary
claimed.
2026-08-06 16:56:30 +02:00
schalli f71e614f7f feat(16-02): display name with fallback in user-detail projections (D-04, UI-SPEC Surface Contract 6)
- ModuleGrantsService.getUserAccess() now selects internalName on the
  membership query's group projection (already present via `include:
  { group: true }` on the grant query)
- Both display points (viaGroups names, membership chips' name field)
  use internalName ?? name; groups[] sorting now runs over the
  displayed name as a result, distinct from GroupsService.listForTenant()
  which still sorts by the raw name column
- 3 new test cases: fallback set/unset, sort-by-displayed-name
- No apps/web/ changes (verified via git diff --name-only)
2026-08-06 16:00:18 +02:00
schalli 253da91ba9 feat(16-02): server-side name lock for imported groups + internalName (D-03/D-04/D-07)
- GroupsService.update() rejects `name` with BadRequestException when the
  loaded group carries a set ldapObjectGuid (imported groups) — a real
  backend invariant, not a UI-only disable
- internalName is settable/clearable on any group; empty/whitespace-only
  values normalize to null instead of an empty display name
- listForTenant() now projects internalName alongside name
- UpdateGroupDto drops ldapDn (D-07: no more codepath binds a local group
  to AD via this route) and gains internalName?: string | null
- 9 new test cases in groups.service.spec.ts (name lock, internalName
  set/clear/idempotent/local-group/unicode, listForTenant projection);
  stale ldapDn update() test removed (behavior intentionally deleted)
2026-08-06 15:58:28 +02:00
schalli 2ef9b8638c feat(16-02): standard group handoff building block (D-06)
- DEFAULT_GROUP_NAME extracted as shared constant between
  ensureDefaultGroup() and the new reassignDefaultBeforeDelete()
- reassignDefaultBeforeDelete(tenantId, groupId) moves the default
  marker deterministically (DEFAULT_GROUP_NAME first, else oldest
  other group by createdAt asc), never deletes, never throws
- 6 test cases covering handoff, fallback ordering, no-other-group,
  non-default no-op, cross-tenant no-op, and P2002 race
2026-08-06 15:55:53 +02:00
schalli 626e29659d feat(16-01): apply Group.internalName/ldapObjectGuid migration to local DB
- Migration 20260806133916_add_group_internal_name_and_object_guid applied
  against the local Postgres container (baselined 24 prior migrations first
  — _prisma_migrations was missing, unrelated to this task's DDL)
- New describe block in migration-sql.spec.ts pins internalName,
  ldapObjectGuid, and the (tenantId, ldapObjectGuid) unique index
- Full API test suite green (40 files, 526 tests)
2026-08-06 15:43:19 +02:00
schalli 9d1254cd78 feat(260805-fok): GroupsService.ensureDefaultGroup(tenantId)
- Neue Methode ensureDefaultGroup: legt fuer einen Mandanten ohne jede
  Gruppe die Standardgruppe 'Alle Benutzer' (isDefault:true) an, nimmt
  alle Bestandsbenutzer als MANUAL-Mitglieder auf und erzeugt Grants
  fuer alle aktiven Module — derselbe Endzustand wie die drei
  Backfill-INSERTs der Migration 20260804130130
- Waechter prueft ausschliesslich group.count === 0, niemals die
  fehlende isDefault-Markierung (D-13)
- P2002 aus dem partiellen Index Group_one_default_per_tenant wird
  abgefangen und liefert null statt zu werfen (Race-Sicherheit)
- groups.service.spec.ts: Fake erweitert um group.count,
  tenantModuleActivation, moduleGrant.findMany/createMany,
  $transaction mit Callback-Form, plus voller ensureDefaultGroup-Testblock
2026-08-05 11:28:16 +02:00
schalli ecadf69e14 feat(260805-d0r): getUserAccess returns groups from GroupMembership (D-16)
- New groupMembership.findMany query, tenant-scoped via group.tenantId
  (GroupMembership has no own tenantId column)
- Response shape changes from an array to { groups, modules }; modules
  entries stay field-identical to before
- Group without any module grant now stays visible, closing the
  reproduced defect
2026-08-05 09:33:51 +02:00
schalli b6d4e4acb6 test(260805-d0r): add failing tests for groups in getUserAccess
- Regression: user in a group without any module grant stays visible
- Cross-tenant: membership in a foreign tenant's group is excluded
- Origin (MANUAL/LDAP), empty-modules case, stable alpha sort
2026-08-05 09:33:22 +02:00
schalli 072fb7f62f feat(15-03): ModuleGrantsController und Einbindung in GroupsModule
- GET /module-grants/matrix, GET /module-grants/users/:userId,
  POST /module-grants, DELETE /module-grants — alle vier rollengeschützt
  (RolesGuard + Roles ADMIN/SUPER_ADMIN)
- matrix vor users/:userId deklariert (Beschattungsfehler-Vermeidung)
- GroupsModule bindet ModuleGrantsController/-Service ein; kein Import
  von ModuleRegistryModule nötig, da der Service nur PrismaService braucht
2026-08-04 18:41:17 +02:00
schalli 5e256db01d feat(15-03): ModuleGrantsService — Freigaben setzen/entziehen mit Mandanten-Gegenprüfung
- assertTargetBelongsToTenant prüft groupId/userId aus dem Request-Body
  gegen tenantId aus dem JWT (T-15-01), vor jedem Grant-Insert
- grant: Entweder-oder-Regel (D-04), aktive TenantModuleActivation (D-02),
  P2002 als Erfolg (Doppelklick-Schutz)
- getMatrix (D-15) und getUserAccess (D-16) für Matrix-Seite und
  Benutzer-Detail, jeweils sortiert und mandantengescoped
- 20 Tests inkl. adjacency/empty/ordering/idempotency/concurrency
2026-08-04 18:41:12 +02:00
schalli 69494d7549 feat(15-02): GroupsModule — CRUD für Gruppen, Mitgliedschaften und Löschauswirkung
- GroupsService: listForTenant/create/update/remove/getImpact/listMembers/addMembers/removeMember/addUserToDefaultGroup, jede Query tenantId-gescoped (T-15-02/T-15-12)
- isDefault:true läuft in einer Transaktion (updateMany+update), D-13
- getImpact liefert { memberCount, grantCount } für den Löschdialog (D-17)
- removeMember beschränkt sich auf source:MANUAL (D-19)
- GroupsController: 8 rollengeschützte Routen unter /groups
- 19 Tests in groups.service.spec.ts, hand-rolled In-Memory-Fake
2026-08-04 15:21:41 +02:00
schalli 92e8eaffa5 feat(15-01): RLS policies for Group/GroupMembership/ModuleGrant (T-15-11)
- Second, deliberately separate migration (pure hand-SQL, no Prisma-
  generated DDL): ENABLE/FORCE ROW LEVEL SECURITY plus a
  tenant_isolation_policy for each of the three new tables, following
  the pattern of 20260618112133_rls_policies (Auth-Kerntabellen)
  rather than the RLS-exempt Tender* app-layer tables
- Group/ModuleGrant compare tenantId directly against
  current_tenant_id(); GroupMembership has no own tenantId and follows
  the PasswordResetToken join pattern (groupId IN (SELECT id FROM
  Group WHERE tenantId = ...))
- migration-sql.spec.ts extended with a second describe block covering
  both migration files (6x ROW LEVEL SECURITY, 3x CREATE POLICY, the
  join vs. direct-comparison shape)
- Re-ran the Task-2 end-to-end proof after applying this migration:
  identical result (USER without grant 403 + empty list, USER with
  direct grant 200 + slug present, ADMIN 200) — the app's DB role
  (tessera) is a Postgres superuser with rolbypassrls=true, so it
  bypasses RLS as documented as an acceptable outcome by the plan;
  RLS remains the defense-in-depth net for any future non-superuser
  connection
2026-08-04 15:11:33 +02:00
schalli c5c704bae9 feat(15-01): Group/GroupMembership/ModuleGrant schema + D-06 backfill migration
- Group/GroupMembership/ModuleGrant models plus MembershipSource enum
  (D-05), placed under TenantModuleActivation with German block comment
- Hand-SQL appended to the generated migration: partial unique index for
  one default group per tenant (D-13), CHECK num_nonnulls xor-constraint
  plus two partial unique indexes for ModuleGrant (D-04), and the D-06
  backfill (Group -> GroupMembership -> ModuleGrant, each INSERT guarded
  by WHERE NOT EXISTS for idempotent re-runs on `prisma migrate deploy`)
- apps/api/src/groups/migration-sql.spec.ts verifies the hand-SQL by
  reading migration.sql directly, no DB required
- Verified against the local DB: default-group count matches tenant
  count, membership/grant counts match existing users/active
  activations, and the XOR constraint rejects a group+user-less insert
2026-08-04 15:03:48 +02:00