166 lines
16 KiB
Markdown
166 lines
16 KiB
Markdown
---
|
|
phase: quick-260910-jab
|
|
verified: 2026-09-10T14:55:00Z
|
|
status: passed
|
|
score: 11/11 must-haves verified
|
|
covered_files:
|
|
- ".planning/WINDOWS.md"
|
|
- ".planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-PLAN.md"
|
|
- ".planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-SUMMARY.md"
|
|
- "apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql"
|
|
- "apps/api/scripts/rls-scratch-check.mjs"
|
|
- "apps/api/src/groups/groups.service.ts"
|
|
- "apps/api/src/groups/migration-sql.spec.ts"
|
|
- "apps/api/src/groups/module-grants.service.spec.ts"
|
|
- "apps/api/src/groups/module-grants.service.ts"
|
|
- "apps/api/src/module-registry/module-access.service.ts"
|
|
- "apps/api/src/prisma/rls-coverage.spec.ts"
|
|
- "apps/api/src/tenders/tender-rss-feed.service.spec.ts"
|
|
- "apps/api/src/tenders/tender-rss-feed.service.ts"
|
|
- "apps/api/src/tenders/tenders.controller.spec.ts"
|
|
- "apps/api/src/tenders/tenders.controller.ts"
|
|
- "docs/mandantentrennung-datenbankrolle.md"
|
|
- "docs/mandantentrennung-etappe2-fehlerrichtung.md"
|
|
- "docs/mandantentrennung-zugriffsklassifikation.md"
|
|
covered_digest: "v1:sha256:4ba76478ddf3d04462ca28c23051e03f469ffd731ae9db235c5cd3e25c803e6f"
|
|
behavior_unverified: 0
|
|
overrides_applied: 0
|
|
---
|
|
|
|
# Quick 260910-jab: Die drei zu kurz greifenden Datenbankregeln — Verification Report
|
|
|
|
**Task Goal:** Close T-JTS-02 (`GroupMembership` checked only the group side),
|
|
T-JTS-03 (`ModuleGrant` checked its own tenant but not the group/user it
|
|
points at) and WINDOWS #19 (nullable `tenantId` would hide platform-wide rows
|
|
from every tenant after cutover) — while leaving the written record coherent.
|
|
|
|
**Verified:** 2026-09-10, independently, against the live database container
|
|
`tessera-ctl-db-1` (address `172.19.0.2`, resolved fresh via `docker inspect`)
|
|
and the working tree at commits `f4f3115`, `6b23735`, `03fb3bf` (base `93444aa`).
|
|
|
|
**Status:** passed
|
|
|
|
## Summary
|
|
|
|
Every priority item in the verification brief was independently re-checked,
|
|
including three destructive falsification experiments (temporarily reverting
|
|
each of the three fixed policies in the migration file and re-running the
|
|
scratch tool against a throwaway database) to prove the inverted checks
|
|
would actually fail if the fix regressed. All three did. Full test suite
|
|
(839/839), type-check (clean), and the scratch tool (74/74) were re-run
|
|
independently and match the SUMMARY's claims exactly. No discrepancy found.
|
|
|
|
## Goal Achievement
|
|
|
|
### Observable Truths
|
|
|
|
| # | Truth | Status | Evidence |
|
|
|---|-------|--------|----------|
|
|
| 1 | `GroupMembership` policy checks both sides (group AND user) | ✓ VERIFIED | Live `pg_policies`: `("groupId" IN (...Group...)) AND ("userId" IN (...User...))`. Falsification: reverted to group-only text in migration file, reran scratch tool → `groupmembership-schreiben-fremder-benutzer-abgelehnt` FAILED as expected, then restored (file byte-identical after restore) |
|
|
| 2 | `ModuleGrant` policy checks both possible targets (group OR user) with null-allowance, both D-04 branches measured | ✓ VERIFIED | Live `pg_policies` shows `tenantId = ... AND (groupId IS NULL OR ...) AND (userId IS NULL OR ...)`. Falsification: reverted to tenant-only text → both `modulegrant-fremde-gruppe-abgelehnt` and `modulegrant-fremder-benutzer-abgelehnt` FAILED as expected, then restored |
|
|
| 3 | `assertTargetBelongsToTenant` in `module-grants.service.ts` unchanged, held by a test that goes red if removed | ✓ VERIFIED | 3 call sites present (`grep -n`); two dedicated spec cases in `module-grants.service.spec.ts:290-312` with an explicit comment (lines 281-289) stating why they'd go red if the app-level guard were dropped |
|
|
| 4 | `TenderRssFeedSource` read/write split: bound read returns own + platform rows; write (insert/update/delete) still requires a tenant; both error directions (too strict / too loose) separately measured | ✓ VERIFIED | Live `pg_policies`: 4 command-separated policies; only `tenant_platform_read_policy` (SELECT) contains `IS NULL`, none of insert/update/delete do. Scratch tool: `tenderrssfeed-plattformzeile-gebunden-sichtbar` + `tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar` (too-strict direction) and `tenderrssfeed-gebundenes-aendern-...-abgelehnt` + `...-loeschen-...-abgelehnt` (too-loose direction), all passed independently |
|
|
| 5 | The three hole-claiming scratch-tool checks are INVERTED, not deleted/loosened, named for the new truth, referencing the old finding | ✓ VERIFIED | Old check names (`...-nicht-verhindert`, `...-trotz-eigener-mandantenkennung-erlaubt`, `...-unter-jedem-mandanten-unsichtbar`) appear only as backward-references inside the new checks' message text, not as separate passing assertions (`grep` over the whole tool file). New names assert rejection/visibility, matching the fix |
|
|
| 6 | `grant-foreign-group` row (dependency for two `module-registry` checks) still gets created, now via the maintenance role, and neither dependent check passes for the wrong reason | ✓ VERIFIED | `runGroupsAreaChecks` creates it via `withAdminPrisma`/BYPASSRLS (line ~913-917) before `runModuleRegistryAreaChecks` runs (both share the same scratch DB in `main()`); `gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus` message text now correctly says the row was excluded because the maintenance-role insert bypassed a rule that would otherwise reject it — not because the rule "lets it through" |
|
|
| 7 | The claim "no application code needs to change" was measured, not assumed; the one path that does (`listForUser`) is bound | ✓ VERIFIED | `listForUser` uses `forTenant(this.prisma, tenantId)`; controller passes `tenantId` from session context (`tenders.controller.ts:267-268`). Falsification: reverted the binding, ran `tender-rss-feed.service.spec.ts` → exactly 1 test failed (`listForUser() bindet...`), 30 passed, matching SUMMARY's claim precisely; restored |
|
|
| 8 | Every record describing one of the three old rules now tells the truth, naming the new migration | ✓ VERIFIED | All 4 in-source header comments (`module-access.service.ts`, `groups.service.ts`, `module-grants.service.ts`, `tender-rss-feed.service.ts` x3) and `rls-coverage.spec.ts` description line updated and name `20260910120000_rls_widen_membership_grant_and_platform_read`; classification doc `tenders` row (35/27) and sum row (107/135) independently re-derived from source via the same grep the doc cites and matched exactly; `rls-access-inventory.spec.ts` (10/10) passes |
|
|
| 9 | WINDOWS #19 closed with evidence naming the migration and both error-direction checks; #18/#20/#21/#23 remain open; what's NOT solved recorded as its own open entry that doesn't vanish with #19 | ✓ VERIFIED | #19 `status: fixed`, `resolved_at` set, description names the migration and 5 named checks, explicitly frames SearchProvider half as REFUTED PREMISE not solved problem. #18/#20/#21/#23 byte-identical to base commit. New entry #24 (open, `deviation`) names both blocked paths (`createPlatform`/`remove`) and states explicitly this predates 260910-jab. Header counts (6 open/17 fixed/1 waived/24 total) match a fresh count of the JSON-equivalent table rows |
|
|
| 10 | Switch stays OFF; `DATABASE_URL` still points at role `tessera` | ✓ VERIFIED | `docker-compose.yml:33` and `.env.example:2` both show role `tessera` (no `_app` suffix), unchanged from base |
|
|
| 11 | Baseline held: test count doesn't drop, type-check clean, scratch tool all-passed, at every task boundary | ✓ VERIFIED | Independently re-ran: 839/839 tests (56 files), `tsc --noEmit` clean, `rls-scratch-check.mjs` 74/74 passed — all match SUMMARY's claimed end-state exactly |
|
|
|
|
**Score:** 11/11 truths verified (0 present-but-behavior-unverified)
|
|
|
|
### Falsification Experiments (independently performed by this verifier)
|
|
|
|
Three destructive experiments were run directly against the scratch tool /
|
|
migration file, each reverted afterward and confirmed byte-identical to the
|
|
original:
|
|
|
|
1. Reverted `GroupMembership` policy text to group-only → `groupmembership-schreiben-fremder-benutzer-abgelehnt` FAILED (1/74). Restored.
|
|
2. Reverted `ModuleGrant` policy text to tenant-only → `modulegrant-fremde-gruppe-abgelehnt` and `modulegrant-fremder-benutzer-abgelehnt` both FAILED (2/74). Restored.
|
|
3. Reverted `TenderRssFeedSource` to the old single-policy form → the tenders-area extraction guard correctly aborted with `tenders-policies-aus-migration-gefunden: FEHLGESCHLAGEN` (fail-closed, not a silent wrong measurement) rather than measuring the old text as if it were current. Restored.
|
|
4. Reverted `TenderRssFeedSourceService.listForUser`'s `forTenant()` binding → exactly 1 test failed (`listForUser() bindet — forTenant() wird mit der uebergebenen Mandantenkennung aufgerufen`), 30 passed. Restored.
|
|
|
|
All four confirm the guarding assertions (tests and scratch checks) genuinely
|
|
detect the regression they claim to detect — not passing by construction.
|
|
|
|
### Required Artifacts
|
|
|
|
| Artifact | Expected | Status | Details |
|
|
|----------|----------|--------|---------|
|
|
| `apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql` | New migration, applied locally | ✓ VERIFIED | Exists, `prisma migrate status` reports up to date, live `pg_policies` text matches file text exactly (diffed by hand for GroupMembership/ModuleGrant/TenderRssFeedSource/SearchProvider) |
|
|
| `apps/api/scripts/rls-scratch-check.mjs` | 3 inverted checks + gegenmessungen + 4 command directions + extraction redirect | ✓ VERIFIED | `readRlsWidenMigrationSql()`/`extractAllPolicySql()` wired into `runGroupsAreaChecks`/`runTendersAreaChecks`; old extraction (`readGroupsRlsPoliciesMigrationSql`) no longer used for GroupMembership/ModuleGrant |
|
|
| `apps/api/src/groups/migration-sql.spec.ts` | New describe block, text-only | ✓ VERIFIED | `rls_widen_membership_grant_and_platform_read migration.sql` block present, 6 tests, all pure regex/string assertions, no DB |
|
|
| `apps/api/src/tenders/tender-rss-feed.service.ts` | `listForUser` bound, 3 header comments corrected | ✓ VERIFIED | Confirmed by reading; `createPlatform`/`remove` deliberately still unbound with corrected reasoning pointing at WINDOWS #24 |
|
|
| `apps/api/src/tenders/tenders.controller.ts` + both spec files | tenant pass-through, two-client proof | ✓ VERIFIED | `extractTriageContext(req)` supplies `tenantId`; two-client proof in spec uses `__makeBoundClient`/`boundCallLog`, not an identity mock (unbound fake doesn't log, bound one does) |
|
|
| `apps/api/src/groups/groups.service.ts`, `module-grants.service.ts`, `module-access.service.ts`, `rls-coverage.spec.ts` | 4 in-source records corrected | ✓ VERIFIED | Read all 4 — each names the new migration and describes the new rule while explicitly retaining the app-level guard as "second net" |
|
|
| `docs/mandantentrennung-zugriffsklassifikation.md` | #19 block answered, 4 inventory rows updated, overview/sum rows re-derived | ✓ VERIFIED | tenders row 35/27 matches independently re-run grep exactly; sum row 107/135 matches; `rls-access-inventory.spec.ts` passes (machine-checked binding to this doc) |
|
|
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | New `## Regelschluss...` section, 5 subsections (r1-r5), annotations at superseded spots | ✓ VERIFIED | All 5 subsections present; r1 quotes an actual 74-check run (verified to match reality); r2 signal table covers both error directions per rule; old measurement blocks preserved verbatim with adjacent `NACHTRAG (260910-jab)` annotations (6+ locations found, not rewritten) |
|
|
| `docs/mandantentrennung-datenbankrolle.md` | The one #19-as-open spot updated | ✓ VERIFIED | Line 91-94 now says GESCHLOSSEN with migration name |
|
|
| `.planning/WINDOWS.md` | #19 fixed, #24 new, counters derived | ✓ VERIFIED | Header counts (6/1/17/24) match row-by-row count; #18/#20/#21/#23 byte-identical to base |
|
|
|
|
### Key Link Verification
|
|
|
|
| From | To | Via | Status | Details |
|
|
|------|-----|-----|--------|---------|
|
|
| `rls-scratch-check.mjs` extraction | `20260910120000_...` migration file | `readRlsWidenMigrationSql()` + `extractAllPolicySql()` | ✓ WIRED | Confirmed by falsification #3 above: reverting the migration file's TenderRssFeedSource text made the tool's own extraction guard fire, proving it reads the live file, not a cached/hardcoded value |
|
|
| Groups-area `grant-foreign-group` provisioning | `module-registry`-area dependent checks | shared scratch DB across `main()`'s sequential area-check calls | ✓ WIRED | Confirmed both dependent checks (`gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus`, `gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit`) pass in the live run and read code confirming ordering |
|
|
| `TendersController.listRssFeeds` | `TenderRssFeedSourceService.listForUser` | `extractTriageContext(req).tenantId` passthrough | ✓ WIRED | Read at `tenders.controller.ts:266-268` |
|
|
| `TenderRssFeedSource` command-separated policies | live database | migration applied via `prisma migrate deploy`, not `migrate dev`/`reset` | ✓ WIRED | `prisma migrate status` → up to date; `pg_policies` text matches file text |
|
|
|
|
### Behavioral Spot-Checks / Probe Execution
|
|
|
|
| Behavior | Command | Result | Status |
|
|
|----------|---------|--------|--------|
|
|
| Scratch tool passes fully | `node apps/api/scripts/rls-scratch-check.mjs` against live container | `Alle 74 Pruefungen bestanden.` | ✓ PASS |
|
|
| Full test suite | `npm --prefix apps/api run test` | 839/839, 56 files | ✓ PASS |
|
|
| Type check | `npm --prefix apps/api run type-check` | clean, no output | ✓ PASS |
|
|
| `rls-access-inventory.spec.ts` (machine clamp doc↔code) | `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` | 10/10 | ✓ PASS |
|
|
| Falsification: GroupMembership reverted | scratch tool against reverted migration text | 1/74 failed (correct check) | ✓ PASS (as regression detector) |
|
|
| Falsification: ModuleGrant reverted | scratch tool against reverted migration text | 2/74 failed (correct checks) | ✓ PASS (as regression detector) |
|
|
| Falsification: TenderRssFeedSource reverted | scratch tool against reverted migration text | extraction guard fired, 1/62 failed | ✓ PASS (fail-closed, not silently wrong) |
|
|
| Falsification: listForUser binding reverted | `npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts` | 1/31 failed (correct test) | ✓ PASS (as regression detector) |
|
|
|
|
### Requirements Coverage
|
|
|
|
| Requirement | Source Plan | Description | Status | Evidence |
|
|
|-------------|-------------|-------------|--------|----------|
|
|
| WINDOWS-19 | 260910-jab-PLAN.md | Platform-wide rows visible to every tenant, not just their own, after cutover | ✓ SATISFIED | 4-policy split live, both error directions measured and independently falsified |
|
|
| T-JTS-02 | 260910-jab-PLAN.md | `GroupMembership` policy checks user side too | ✓ SATISFIED | Live policy text confirmed, falsified |
|
|
| T-JTS-03 | 260910-jab-PLAN.md | `ModuleGrant` policy checks referenced group/user, not just own tenant | ✓ SATISFIED | Live policy text confirmed, falsified, both D-04 branches measured |
|
|
|
|
### Anti-Patterns Found
|
|
|
|
None. Searched all 16 files in `files_modified` for `TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER` — zero hits. No stray stub/empty-return patterns found in the reviewed code.
|
|
|
|
### Scope Discipline
|
|
|
|
- `apps/api/prisma/schema.prisma` — untouched (`git diff --stat` empty against base)
|
|
- `docker-compose*.yml`, `.env.example`, `.env.prod.example` — untouched
|
|
- `DATABASE_URL` still role `tessera` (no `_app` suffix) — switch remains OFF
|
|
- `assertTargetBelongsToTenant` present with 3 call/definition sites, unchanged behavior
|
|
- WINDOWS #21, #23 — byte-identical to base commit (not touched by this task)
|
|
- Full diff scope (16 files changed) matches the plan's declared `files_modified` list exactly, plus `module-grants.service.spec.ts` (listed in SUMMARY key-files, consistent with Aufgabe 2's test-first requirement)
|
|
|
|
### Human Verification Required
|
|
|
|
None. All must-haves are verifiable via the live database, the scratch tool,
|
|
and static analysis, and were independently re-derived rather than accepted
|
|
from the SUMMARY.
|
|
|
|
### Gaps Summary
|
|
|
|
No gaps found. This is an unusually well-evidenced quick task: every claim in
|
|
the SUMMARY that could be independently re-measured was re-measured (not
|
|
re-read), including four destructive falsification experiments this verifier
|
|
ran fresh (beyond the two the executor already ran), and every number
|
|
matched exactly (74/74, 839/839, 35/27, 107/135, 6/17/1/24). The framing of
|
|
the SearchProvider half of WINDOWS #19 as a refuted premise (rather than a
|
|
solved problem) is accurate and consistent across the migration header, the
|
|
ledger entry, and the classification document.
|
|
|
|
---
|
|
|
|
_Verified: 2026-09-10T14:55:00Z_
|
|
_Verifier: Claude (gsd-verifier)_
|