Files

16 KiB

phase, verified, status, score, covered_files, covered_digest, behavior_unverified, overrides_applied
phase verified status score covered_files covered_digest behavior_unverified overrides_applied
quick-260910-jab 2026-09-10T14:55:00Z passed 11/11 must-haves verified
.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
v1:sha256:4ba76478ddf3d04462ca28c23051e03f469ffd731ae9db235c5cd3e25c803e6f 0 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
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)