feat(260924-m4n): Bilderrahmen Stufe 2 - alte Bildspalte data entfernt, storagePath Pflicht

- Migration 20260924120000_dashboard_image_drop_data: Schutzpruefung
  (bricht ab, solange eine Zeile ohne storagePath existiert; row_security
  aus, damit ein Eigentuemer ohne BYPASSRLS nicht still 0 Zeilen sieht),
  dann NOT NULL, DROP COLUMN data, DROP POLICY system_read_policy
- Dienst: Bootstrap-Umzug samt forSystem() und Selbstheilung aus data
  entfernt; Upload vergibt die UUID selbst, Zeile gleich mit Pfad
- FORSYSTEM_ALLOWED_CALL_SITES, Tests, Zugriffsklassifikation (per
  Gate-Schleife gemessen: 61/213/6) nachgezogen
- Betriebshandbuch Kap. 4: Hinweis und Wiederherstellungsweg bei Abbruch
- Todo 2026-09-22 nach completed/

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 16:09:36 +02:00
parent b10734f382
commit dd54ec5d42
9 changed files with 230 additions and 288 deletions
@@ -0,0 +1,53 @@
-- quick-260924-m4n — Stufe 2 der Umstellung aus quick-260922-hk4: die alte
-- Bildspalte "data" faellt, "storagePath" wird Pflicht.
--
-- Stufe 1 (20260922120000_dashboard_image_to_disk) hat "storagePath"
-- angelegt und "data" nur NULLbar gemacht, weil `prisma migrate deploy` VOR
-- dem Anwendungsstart laeuft: ein sofortiges DROP haette die Bytes
-- vernichtet, bevor der Bootstrap-Umzug (DashboardImagesService,
-- Version 1.3.1) sie auf die Platte schreiben konnte (T-HK4-03). Dieser
-- Umzug ist mit dieser Version aus dem Code entfernt.
--
-- SCHUTZ VOR DATENVERLUST: gibt es noch eine Zeile ohne "storagePath", hat
-- der Umzug auf diesem Server nie gearbeitet (der Server hat eine Version
-- < 1.3.1 uebersprungen). Dann bricht die Migration mit einer Meldung ab,
-- BEVOR irgendetwas geaendert wird (die Pruefung steht vor jeder Aenderung),
-- `migrate deploy` stoppt, die API startet nicht. Abhilfe
-- (docs/anleitung-betrieb.md, Kapitel 4, gemessen in einer Wegwerf-DB):
-- den fehlgeschlagenen Eintrag in "_prisma_migrations" als zurueckgenommen
-- vermerken (sonst verweigert auch 1.3.1 den Start mit P3009), dann eine
-- Version >= 1.3.1 einmal starten lassen (der Umzug laeuft beim Start von
-- selbst), danach erneut auf diese Version gehen.
--
-- ZEILENSCHUTZ (RLS) UND DIE PRUEFUNG: "DashboardImage" hat FORCE ROW LEVEL
-- SECURITY (20260921120000). FORCE wirkt auch auf den Tabelleneigentuemer —
-- ohne Sitzungsvariablen wuerde die Mandantenregel dem EXISTS jede Zeile
-- wegfiltern, die Pruefung saehe 0 Zeilen und der Schutz waere stumm
-- wirkungslos. Heute laeuft die Migration als `tessera` (Superuser mit
-- BYPASSRLS, gemessen 24.09.2026 lokal) und sieht alles. Fuer den Fall, dass
-- sie spaeter ueber TESSERA_MIGRATE_DATABASE_URL als Eigentuemer OHNE
-- BYPASSRLS laeuft (docs/mandantentrennung-datenbankrolle.md), schaltet die
-- Pruefung `row_security` fuer diese Transaktion ab: PostgreSQL filtert dann
-- NICHT still, sondern bricht mit "query would be affected by row-level
-- security policy" ab. Die Pruefung sieht also entweder alle Zeilen oder
-- scheitert laut — nie "0 gesehen, weiter".
DO $$
BEGIN
PERFORM set_config('row_security', 'off', true);
IF EXISTS (SELECT 1 FROM "DashboardImage" WHERE "storagePath" IS NULL) THEN
RAISE EXCEPTION 'DashboardImage: es gibt noch Zeilen ohne storagePath — Umzug (quick-260922-hk4) zuerst mit einer Version >= 1.3.1 laufen lassen, dann erneut deployen';
END IF;
PERFORM set_config('row_security', 'on', true);
END $$;
ALTER TABLE "DashboardImage" ALTER COLUMN "storagePath" SET NOT NULL;
ALTER TABLE "DashboardImage" DROP COLUMN "data";
-- Die Systemkontext-Leseregel aus 20260922120000 hatte genau einen Zweck:
-- den Bootstrap-Umzug, der ueber ALLE Mandanten las (`forSystem()`). Der
-- Umzug ist entfernt, niemand liest "DashboardImage" mehr systemgebunden —
-- eine offene Leseregel ohne Leser waere nur Angriffsflaeche. Es bleibt
-- allein "tenant_isolation_policy" (Mandant UND Benutzer). IF EXISTS, damit
-- die Migration auch auf einer Datenbank durchlaeuft, auf der die Regel von
-- Hand entfernt wurde.
DROP POLICY IF EXISTS system_read_policy ON "DashboardImage";