diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 97319a7..58499c9 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -65,7 +65,7 @@ - [x] **PERM-01**: Admin verwaltet Gruppen pro Mandant (anlegen, umbenennen, löschen) und weist Benutzer manuell zu oder entfernt sie. Beim Löschen einer belegten Gruppe warnt ein Dialog mit Mitglieder- und Freigabenanzahl, bevor Mitgliedschaften und Freigaben mitgelöscht werden. - [ ] **PERM-02**: Eine Gruppe kann optional an einen AD-Gruppen-DN gebunden werden, ausgewählt aus der bestehenden LDAP-Gruppensuche. Der Benutzer-Sync liest `memberOf` mit und pflegt daraus die Mitgliedschaften; verlässt ein Benutzer die AD-Gruppe, fällt nur seine LDAP-Mitgliedschaft weg — manuell gesetzte bleiben bestehen. -- [ ] **PERM-03**: Admin gibt ein mandantenweit aktives Modul gezielt für Gruppen und für einzelne Benutzer frei und entzieht Freigaben wieder. Gruppenfreigaben werden in einer Matrix Module × Gruppen gepflegt, Einzelfreigaben im Benutzer-Detail samt Anzeige der über Gruppen geerbten Rechte. +- [x] **PERM-03**: Admin gibt ein mandantenweit aktives Modul gezielt für Gruppen und für einzelne Benutzer frei und entzieht Freigaben wieder. Gruppenfreigaben werden in einer Matrix Module × Gruppen gepflegt, Einzelfreigaben im Benutzer-Detail samt Anzeige der über Gruppen geerbten Rechte. - [x] **PERM-04**: Ohne Freigabe hat ein USER keinen Zugriff: das Modul fehlt in der Sidebar, die Modulseite antwortet mit einer 403-Seite samt Hinweis, und die Modul-API antwortet mit 403. Sidebar, Modulseite und API nutzen dieselbe Zugriffsauflösung. - [x] **PERM-05**: ADMIN und SUPER_ADMIN sehen und nutzen innerhalb ihres Mandanten alle aktiven Module ohne Freigabe. - [x] **PERM-06**: Die Migration überführt den Bestand ohne Zugriffsverlust: pro Mandant entsteht eine als Standardgruppe markierte Gruppe mit allen bestehenden Benutzern und Freigaben für alle zum Migrationszeitpunkt aktiven Module. Neue Benutzer — manuell angelegt wie per LDAP importiert — treten der markierten Standardgruppe automatisch bei. @@ -124,8 +124,8 @@ | PERM-01 | Phase 15 | Pending | | PERM-02 | Phase 15 | Pending | -| PERM-03 | Phase 15 | Pending | -| PERM-04 | Phase 15 | Pending | +| PERM-03 | Phase 15 | Complete | +| PERM-04 | Phase 15 | Complete | | PERM-05 | Phase 15 | Pending | | PERM-06 | Phase 15 | Pending | | PERM-07 | Phase 15 | Complete | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index bcb1113..067ef29 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -511,7 +511,7 @@ Plans: **Neue Modelle**: `Group` (tenantId, name, ldapDn?), `GroupMembership` (userId, groupId, source MANUAL|LDAP), `ModuleGrant` (tenantId, moduleId, groupId? | userId?) -**Plans**: 3/8 plans executed +**Plans**: 4/8 plans executed Plans: **Wave 1** @@ -526,7 +526,7 @@ Plans: **Wave 3** *(blocked on Wave 2 completion)* -- [ ] 15-03-PLAN.md — Freigabe-API für Gruppen und Benutzer plus Modulkatalog-Endpoint +- [x] 15-03-PLAN.md — Freigabe-API für Gruppen und Benutzer plus Modulkatalog-Endpoint - [ ] 15-06-PLAN.md — Gruppenverwaltung im Admin-UI und alle i18n-Schlüssel der Phase **Wave 4** *(blocked on Wave 3 completion)* @@ -559,4 +559,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 | 12. Tender Notifications | 4/4 | In Progress| | | 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | In Progress| | | 14. RSS, Email-Alert Ingestion & Module Rollout | 5/5 | In Progress| | -| 15. Modul-Berechtigungen: Gruppen & User-Grants | 3/8 | In Progress| | +| 15. Modul-Berechtigungen: Gruppen & User-Grants | 4/8 | In Progress| | diff --git a/.planning/STATE.md b/.planning/STATE.md index 3421e8c..c59a884 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar current_phase: 15 current_phase_name: modul-berechtigungen-gruppen-user-grants status: executing -stopped_at: Completed 15-05-PLAN.md -last_updated: "2026-08-04T13:39:18.710Z" +stopped_at: Completed 15-03-PLAN.md +last_updated: "2026-08-04T16:43:23.536Z" last_activity: 2026-08-04 last_activity_desc: Phase 15 execution started progress: total_phases: 15 completed_phases: 13 total_plans: 75 - completed_plans: 69 + completed_plans: 70 --- # Project State @@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17) ## Current Position Phase: 15 (modul-berechtigungen-gruppen-user-grants) — EXECUTING -Plan: 4 of 8 +Plan: 5 of 8 Status: Ready to execute Last activity: 2026-08-04 — Phase 15 execution started -Progress: [█████████░] 92% +Progress: [█████████░] 93% ## Performance Metrics @@ -103,6 +103,7 @@ Progress: [█████████░] 92% | Phase 15 P01 | 24min | 3 tasks | 10 files | | Phase 15-modul-berechtigungen-gruppen-user-grants P02 | 11min | 2 tasks | 11 files | | Phase 15 P05 | 9min | 2 tasks | 5 files | +| Phase 15 P03 | 32min | 3 tasks | 8 files | ## Accumulated Context @@ -239,6 +240,9 @@ Recent decisions affecting current work: - [Phase ?]: [15-02]: UserService.create ist die einzige Codestelle für D-11/D-12 — LdapService erbt die Regel ohne eigene Kopie (ldap.service.ts unverändert) - [Phase ?]: [15-05]: WIDGET_MODULE_MAP bleibt am Ende dieser Phase bewusst leer (D-22) — kein neuer Widget-Typ, keine Schemaänderung, nur die Filtermechanik - [Phase ?]: [15-05]: vi.hoisted() für die je-Testfall mutierbare WIDGET_MODULE_MAP-Mock-Referenz — vi.mock wird an den Dateianfang gehoben, ein normaler top-level const wäre zur Factory-Ausführungszeit noch nicht initialisiert +- [Phase ?]: [15-03]: assertTargetBelongsToTenant als eigenständige Cross-Tenant-Prüfung eines referenzierten Fremdobjekts vor jedem Grant-Insert (T-15-01), kein Vorbild im Bestandscode +- [Phase ?]: [15-03]: Kein Import von ModuleRegistryModule in GroupsModule — ModuleGrantsService injiziert ausschließlich PrismaService +- [Phase ?]: [15-03]: getCatalogFlags liefert Map nur für aktive Module, Controller mappt fehlenden Eintrag auf beide Flags false ### Pending Todos @@ -280,7 +284,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-08-04T13:39:18.682Z -Stopped at: Completed 15-05-PLAN.md +Last session: 2026-08-04T16:43:23.505Z +Stopped at: Completed 15-03-PLAN.md Resume file: None Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created diff --git a/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-03-SUMMARY.md b/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-03-SUMMARY.md new file mode 100644 index 0000000..8003bc7 --- /dev/null +++ b/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-03-SUMMARY.md @@ -0,0 +1,180 @@ +--- +phase: 15-modul-berechtigungen-gruppen-user-grants +plan: 03 +subsystem: auth +tags: [nestjs, prisma, module-access, groups, grants, vitest] + +# Dependency graph +requires: + - phase: 15-modul-berechtigungen-gruppen-user-grants + plan: 01 + provides: Group/GroupMembership/ModuleGrant-Schema, ModuleAccessService.getAccessibleModuleIds (D-01) als Single Source of Truth + - phase: 15-modul-berechtigungen-gruppen-user-grants + plan: 02 + provides: GroupsModule/GroupsService (CRUD, Ownership-Check-Muster), an das dieser Plan Controller und Service anhängt +provides: + - ModuleGrantsService.grant/revoke — Schreibseite der Modul-Freigaben (PERM-03), mit assertTargetBelongsToTenant als Cross-Tenant-Grant-Injection-Schutz vor jedem Insert + - ModuleGrantsService.getMatrix — Datenlieferung für die Freigabe-Matrix-Seite (D-15), konsumiert von Plan 15-06/15-07 + - ModuleGrantsService.getUserAccess — Datenlieferung für das Benutzer-Detail (D-16), zeigt geerbte Gruppen + Direkt-Grant + - ModuleGrantsController — vier rollengeschützte /module-grants-Routen + - ModuleAccessService.getCatalogFlags + GET /modules/catalog — beide Statusflags (isActiveForTenant, hasAccess) in einer Antwort für den Marketplace (D-08), konsumiert von Plan 15-08 +affects: [15-06, 15-07, 15-08] + +actuals: + tokens: 9438 + tasks: 3 + commits: 3 + +tech-stack: + added: [] + patterns: + - "assertTargetBelongsToTenant als eigenständige Cross-Tenant-Prüfung eines REFERENZIERTEN Fremdobjekts (nicht nur direktes Eigentum wie DashboardService.removeWidget) — kein Vorbild im Bestandscode, neu etabliert für Grant-Ziele" + - "P2002 als Erfolgspfad: create() wirft, catch fängt den Fehlercode ab und liefert per findFirst den bestehenden Datensatz zurück statt eines HTTP 500 — Fortführung des in GroupsService.create etablierten Verfahrens" + - "getCatalogFlags liefert eine Map nur für aktive Module; ein fehlender Eintrag wird im Controller auf beide Flags false gemappt, statt die Map für jedes registrierte Modul vorzubefüllen" + +key-files: + created: + - apps/api/src/groups/module-grants.service.ts + - apps/api/src/groups/module-grants.service.spec.ts + - apps/api/src/groups/module-grants.controller.ts + - apps/api/src/groups/dto/create-module-grant.dto.ts + modified: + - apps/api/src/groups/groups.module.ts + - apps/api/src/module-registry/module-access.service.ts + - apps/api/src/module-registry/module-access.service.spec.ts + - apps/api/src/module-registry/module-registry.controller.ts + +key-decisions: + - "Kein Import von ModuleRegistryModule in GroupsModule — ModuleGrantsService injiziert ausschließlich PrismaService, die im Plan-Action-Text als 'falls benötigt' formulierte Ergänzung war nicht nötig" + - "grant prüft die aktive TenantModuleActivation ausschließlich über tenantModuleActivation.findUnique (kein zusätzlicher module.findUnique-Existenzcheck) — eine nicht existierende moduleId fällt bereits über die fehlende Aktivierung in dieselbe BadRequestException, ein zweiter Query wäre redundant" + +requirements-completed: [PERM-03, PERM-04] + +coverage: + - id: D1 + description: "ModuleGrantsService.grant/revoke mit assertTargetBelongsToTenant vor jedem Insert (T-15-01), Entweder-oder-Regel (D-04), aktive TenantModuleActivation als Voraussetzung (D-02), P2002 als Erfolg" + requirement: "PERM-03" + verification: + - kind: unit + ref: "apps/api/src/groups/module-grants.service.spec.ts (20 Tests, deckt jeden -Fall inkl. adjacency/empty/ordering/idempotency/concurrency)" + status: pass + - kind: e2e + ref: "curl gegen laufende lokale API: POST /module-grants mit groupId eines fremden Test-Mandanten -> 404, Datensatz unangetastet; Gruppen-Grant gesetzt -> GET /modules/tender-radar als Gruppenmitglied 200; Grant entzogen -> derselbe Aufruf sofort 403 (D-09, ohne Zwischenschritt)" + status: pass + human_judgment: false + - id: D2 + description: "getMatrix (D-15) und getUserAccess (D-16) liefern Freigabe-Matrix- und Benutzer-Detail-Daten in je einer Antwort, sortiert und mandantengescoped" + requirement: "PERM-03" + verification: + - kind: unit + ref: "apps/api/src/groups/module-grants.service.spec.ts — getMatrix/getUserAccess-Testgruppen" + status: pass + - kind: e2e + ref: "curl gegen laufende lokale API: GET /module-grants/matrix als ADMIN -> 200 mit modules/groups/grants, als USER -> 403; GET /module-grants/users/:userId zeigt viaGroups=['Alle Benutzer'] + direct:false für den Testbenutzer" + status: pass + human_judgment: false + - id: D3 + description: "ModuleAccessService.getCatalogFlags + GET /modules/catalog liefert isActiveForTenant und hasAccess in einer Antwort (D-08), ADMIN/SUPER_ADMIN sehen hasAccess immer wahr für aktive Module (D-03)" + requirement: "PERM-04" + verification: + - kind: unit + ref: "apps/api/src/module-registry/module-access.service.spec.ts (4 neue getCatalogFlags-Tests)" + status: pass + - kind: e2e + ref: "curl gegen laufende lokale API: GET /modules/catalog als USER ohne Gruppen-Mitgliedschaft zeigt tender-radar mit isActiveForTenant:true, hasAccess:false; derselbe Aufruf als ADMIN zeigt beide Flags true" + status: pass + human_judgment: false + +duration: 32min +completed: 2026-08-04 +status: complete +--- + +# Phase 15 Plan 03: Modul-Freigaben — Schreibseite, Matrix, Benutzer-Detail, Katalog Summary + +**`ModuleGrantsService`/`ModuleGrantsController` für Gruppen- und Direkt-Grants mit einer eigens etablierten Cross-Tenant-Gegenprüfung vor jedem Insert (`assertTargetBelongsToTenant`, T-15-01), die Datenlieferung für Freigabe-Matrix (D-15) und Benutzer-Detail (D-16), und `GET /modules/catalog`, das dem Marketplace beide Statusflags (`isActiveForTenant`, `hasAccess`) in einer Antwort liefert (D-08) — alles end-to-end gegen die laufende lokale API bewiesen, inklusive des sofortigen Entzugs ohne Zwischenschritt (D-09).** + +## Performance + +- **Duration:** 32 min +- **Started:** 2026-08-04T16:28:00Z +- **Completed:** 2026-08-04T18:41:00Z (inkl. manueller E2E-Verifikation) +- **Tasks:** 3 +- **Files modified:** 8 + +## Accomplishments + +- `ModuleGrantsService` mit `grant`, `revoke`, `getMatrix`, `getUserAccess` und der privaten `assertTargetBelongsToTenant` — der zentrale Schutz gegen mandantenübergreifende Freigaben (T-15-01). Für dieses Muster gab es kein Vorbild im Bestandscode: bisherige Ownership-Checks (`DashboardService.removeWidget`, `GroupsService.findOwned`) prüfen nur direktes Eigentum, nicht eine zweite Mandantengrenze über eine referenzierte Relation +- `grant` prüft in fester Reihenfolge: Entweder-oder von `groupId`/`userId` (D-04, `BadRequestException` mit Klartext statt rohem Postgres-Constraint-Namen), dann die Mandanten-Gegenprüfung, dann die aktive `TenantModuleActivation` (D-02), dann `create`. Ein `P2002` aus dem partiellen Unique-Index (zwei parallele Klicks auf dieselbe Matrix-Zelle) wird abgefangen und liefert per `findFirst` den bestehenden Datensatz zurück — kein HTTP 500 bei Doppelklick oder echter Nebenläufigkeit +- `revoke` nutzt `deleteMany` mit `tenantId` im `where` als IDOR-Schutz (T-15-02) — ein Ziel eines fremden Mandanten trifft null Zeilen, kein vorheriger Lookup nötig +- `getMatrix(tenantId)` liefert `{ modules, groups, grants }` in einer Antwort: aktive Module nach category+name, Gruppen nach name, Gruppen-Grants als Paare aus moduleId/groupId — deterministisch über wiederholte Aufrufe +- `getUserAccess(tenantId, userId)` liefert je aktivem Modul die Namen der Gruppen, über die der Benutzer erbt, und ein Direkt-Grant-Kennzeichen — Gruppen-Grant und Direkt-Grant auf dasselbe Modul erscheinen gleichzeitig, keiner verdrängt den anderen (Union statt Vorrang) +- `ModuleGrantsController` bildet vier rollengeschützte Routen unter `/module-grants` ab (`GET matrix`, `GET users/:userId`, `POST`, `DELETE`); `matrix` ist vor `users/:userId` deklariert (Beschattungsfehler-Vermeidung, Projekt hatte diesen Fehler schon einmal) +- `ModuleAccessService.getCatalogFlags(tenantId, userId, role)` und `ModuleRegistryController.findCatalog` (`GET /modules/catalog`) liefern je registriertem Modul beide Statusflags in einer Antwort (D-08) — für ADMIN/SUPER_ADMIN ist `hasAccess` bei jedem aktiven Modul wahr (D-03), weil `getAccessibleModuleIds` den Rollen-Kurzschluss anwendet +- Jede erfolgreiche `grant`-/`revoke`-Operation schreibt eine Logzeile über `this.logger` (D-23) — keine Audit-Tabelle, keine Ansicht im Admin-UI +- D-04 vollständig eingehalten: der Datensatz trägt kein Rechtestufen-Feld, und der Service bietet keine Methode, die eines setzen könnte +- End-to-End-Nachweis gegen die laufende lokale API (`dist/main` gegen die DB-Container-IP, `DATABASE_URL`/`JWT_SECRET` wie in vorherigen Plänen dieser Phase): `GET /module-grants/matrix` als ADMIN → 200 mit `modules`/`groups`/`grants`, als USER → 403; `POST /module-grants` mit `groupId` eines eigens angelegten Test-Fremdmandanten → 404, kein Datensatz angelegt; `GET /modules/catalog` als USER (ohne Gruppen-Mitgliedschaft) zeigt `tender-radar` mit `isActiveForTenant:true, hasAccess:false`, derselbe Aufruf als ADMIN zeigt beide Flags `true`; Testbenutzer der Gruppe „Alle Benutzer“ hinzugefügt → `GET /modules/tender-radar` 200; Grant entzogen → derselbe Aufruf sofort 403 ohne Zwischenschritt (D-09); `GET /module-grants/users/:userId` zeigt `viaGroups:['Alle Benutzer'], direct:false` + +## Task Commits + +Jeder Task wurde atomar committet: + +1. **Task 1: ModuleGrantsService — Freigaben setzen und entziehen mit Mandanten-Gegenprüfung** - `5e256db` (feat) +2. **Task 2: ModuleGrantsController und Einbindung in GroupsModule** - `072fb7f` (feat) +3. **Task 3: GET /modules/catalog — beide Statusflags in einer Antwort für den Marketplace** - `1c32543` (feat) + +**Plan metadata:** siehe Commit dieser SUMMARY.md (docs: complete plan) + +## Files Created/Modified + +- `apps/api/src/groups/module-grants.service.ts` - `ModuleGrantsService` (neu): grant/revoke/getMatrix/getUserAccess/assertTargetBelongsToTenant +- `apps/api/src/groups/module-grants.service.spec.ts` - 20 Tests, hand-rolled In-Memory-Prisma-Fake (Projekt-Konvention) +- `apps/api/src/groups/dto/create-module-grant.dto.ts` - `CreateModuleGrantDto` (moduleId required, groupId/userId optional) +- `apps/api/src/groups/module-grants.controller.ts` - `ModuleGrantsController` (neu), vier rollengeschützte `/module-grants`-Routen +- `apps/api/src/groups/groups.module.ts` - `ModuleGrantsController`/`ModuleGrantsService` in controllers/providers/exports ergänzt +- `apps/api/src/module-registry/module-access.service.ts` - `getCatalogFlags` ergänzt +- `apps/api/src/module-registry/module-access.service.spec.ts` - 4 neue `getCatalogFlags`-Tests +- `apps/api/src/module-registry/module-registry.controller.ts` - `findCatalog`-Handler (`GET /modules/catalog`) ergänzt + +## Decisions Made + +- Kein Import von `ModuleRegistryModule` in `GroupsModule` — der Plan-Action-Text formulierte dies als „falls der Service dessen Registry-Methoden benötigt“; `ModuleGrantsService` injiziert ausschließlich `PrismaService`, der Import wäre unbenutzte Kopplung gewesen +- `grant` prüft die Modul-Aktivierung ausschließlich über `tenantModuleActivation.findUnique`, ohne separaten `module.findUnique`-Existenzcheck — eine nicht existierende `moduleId` landet bereits über die fehlende Aktivierung in derselben `BadRequestException`, ein zweiter Query wäre redundant gewesen und ist im ``-Block auch nicht gefordert + +## Deviations from Plan + +**1. [Sonstiges — Akzeptanzkriterium-Diskrepanz, keine Rule 1-4] DTO-Grep-Kriterium zählt 2 statt 3** + +- **Found during:** Task 1, beim Verifizieren der Akzeptanzkriterien nach dem Schreiben von `create-module-grant.dto.ts` +- **Issue:** Das Akzeptanzkriterium `grep -cE '(moduleId|groupId|userId)[?]?: string' ... gibt 3 aus` erwartet implizit ein Pflichtfeld ohne Definite-Assignment-Assertion (`moduleId: string`). Das Projekt erzwingt aber `strict: true` (inkl. `strictPropertyInitialization`) in `tsconfig.base.json`, und jede bestehende DTO im Projekt (`CreateGroupDto`, `AddGroupMembersDto`, `CreateWidgetDto` u. a.) schreibt Pflichtfelder deshalb als `feld!: string`. Mit `moduleId!: string;` liefert der Grep nur 2 Treffer (`groupId?:`, `userId?:`), weil das `!` zwischen Feldname und `:` steht und die Regex `[?]?` nur ein optionales `?` erlaubt +- **Warum keine Codeänderung:** `moduleId: string;` ohne `!` bricht `tsc --noEmit` mit `TS2564: Property 'moduleId' has no initializer` — das zweite, härtere Akzeptanzkriterium desselben Tasks. Die beiden Kriterien stehen im Widerspruch; die Projekt-Konvention (`!` für Pflichtfelder) und ein sauberer `type-check` wiegen schwerer als der wörtliche Grep-Zähler eines einzelnen Akzeptanzkriteriums +- **Verifiziert stattdessen:** `pnpm --filter @tessera/api run type-check` läuft fehlerfrei, alle 20 Service-Tests inkl. der Entweder-oder-/XOR-Fälle sind grün, und `grep -cE '^\s+[a-zA-Z]+[?]?:' ...` zeigt weiterhin genau die drei erwarteten Feldnamen (kein viertes Feld) +- **Files modified:** `apps/api/src/groups/dto/create-module-grant.dto.ts` +- **Commit:** `5e256db` + +## Issues Encountered + +- Für den End-to-End-Nachweis lief zunächst ein veralteter `dist/main`-Prozess aus einer vorherigen Sitzung (Build-Stand von 15-05, 15:36 Uhr) auf Port 3001 — beendet, `pnpm --filter @tessera/api build` neu ausgeführt, mit denselben `DATABASE_URL`/`JWT_SECRET`-Werten wie die vorherige Sitzung (Container-IP `172.19.0.2`, `tessera:tessera_dev`) neu gestartet +- Für den Fremdmandanten-Nachweis (T-15-01) wurde per `psql` ein zweiter, isolierter Test-Tenant samt Gruppe und Benutzer angelegt (analog zum in 15-02 etablierten Verfahren) — nach dem Nachweis vollständig wieder aus der DB entfernt +- Für den D-09-Nachweis (sofortiger Entzug) wurde vorübergehend der einzige produktionsnahe Gruppen-Grant (aus dem D-06-Backfill: „Alle Benutzer“ → `tender-radar`) entzogen und danach exakt wiederhergestellt — die lokale DB enthält nach diesem Plan denselben `ModuleGrant`-Datensatz wie zuvor (neue `id`, identische `tenantId`/`moduleId`/`groupId`) +- Zwei Testbenutzer (`test-admin-1503`, `test-user-1503`) und ein Test-Fremdmandant (`test-foreign-1503` samt Tenant/Gruppe) wurden ausschließlich für die Dauer der Verifikation angelegt und danach vollständig entfernt — `SELECT * FROM "User"`/`"Tenant"` zeigt nach dem Aufräumen wieder exakt den Ausgangsstand (1 Benutzer, 1 Mandant) +- `isDefault` der Gruppe „Alle Benutzer“ war bereits vor diesem Plan `false` (siehe `local_db_note` im Auftrag) — dieser Plan hat daran nichts geändert und war davon nicht betroffen, da `grant`/`revoke`/`getMatrix`/`getUserAccess` nicht auf `isDefault` zugreifen + +## User Setup Required + +None - keine externe Service-Konfiguration nötig. + +## Next Phase Readiness + +- `ModuleGrantsService.getImpact`-Äquivalent für Freigaben existiert nicht in diesem Plan (nicht gefordert) — Plan 15-06/15-07 bauen die Admin-Oberflächen (Matrix-Seite, Benutzer-Detail) direkt auf `getMatrix`/`getUserAccess`/`grant`/`revoke` auf +- `GET /modules/catalog` ist bereit für `MarketplaceCard` (Plan 15-08) — beide Flags kommen in einer Antwort, keine Karte kann kurzzeitig ohne Sperrhinweis klickbar erscheinen +- Kein Bestandsverhalten gebrochen: volle API-Testsuite (493/493) grün, `type-check` fehlerfrei +- Kein offener Blocker aus diesem Plan + +--- +*Phase: 15-modul-berechtigungen-gruppen-user-grants* +*Completed: 2026-08-04* + +## Self-Check: PASSED + +Alle in dieser SUMMARY genannten Dateien existieren auf der Platte, alle genannten Commit-Hashes (`5e256db`, `072fb7f`, `1c32543`) sind im Git-Log auffindbar.