-- ============================================================================= -- Phase 3: Mandantentrennung, Koordinaten, Berichtsfreigabe, Indizes. -- -- Vier Dinge, die in der Datenbank besser aufgehoben sind als im Code: -- -- 1. Row Level Security für die acht neuen Tabellen. -- -- 2. Der Gleichlauf von Dezimalgrad und PostGIS-Punkt. Die Anwendung -- schreibt Breite und Länge als Zahl; den geography-Punkt setzt ein -- Trigger. Sonst müsste jede Schreiboperation Roh-SQL sein, und die -- Karte ab Phase 6 fände Einsätze, die über einen anderen Weg entstanden -- sind, nicht wieder. -- -- 3. Die Unveränderbarkeit freigegebener Einsatzberichte. Der Bericht geht -- an die Gemeinde und im Schadensfall an die Versicherung. Dieselbe Logik -- wie beim Prüfprotokoll aus Phase 2. -- -- 4. Suchindizes und ein Teilindex auf die laufenden Einsätze. -- ============================================================================= -- ----------------------------------------------------------------------------- -- Row Level Security -- ----------------------------------------------------------------------------- DO $$ DECLARE tabelle text; BEGIN FOREACH tabelle IN ARRAY ARRAY[ 'einsatz', 'einsatzfahrzeug', 'einsatzkraft', 'materialverbrauch', 'lagemeldung', 'portal_zugang', 'uebung', 'uebungsteilnahme' ] LOOP EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tabelle); EXECUTE format( 'CREATE POLICY %I ON %I FOR ALL ' 'USING ("organisationId" = aktuelle_organisation()) ' 'WITH CHECK ("organisationId" = aktuelle_organisation())', tabelle || '_mandant', tabelle ); END LOOP; END; $$; -- ----------------------------------------------------------------------------- -- Koordinaten: Dezimalgrad und PostGIS-Punkt zusammenhalten -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION einsatz_koordinaten_setzen() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW."breitengrad" IS NULL OR NEW."laengengrad" IS NULL THEN NEW."koordinaten" := NULL; RETURN NEW; END IF; -- Reihenfolge beachten: ST_MakePoint nimmt erst die Länge, dann die Breite. NEW."koordinaten" := ST_SetSRID( ST_MakePoint(NEW."laengengrad"::double precision, NEW."breitengrad"::double precision), 4326 )::geography; RETURN NEW; END; $$; COMMENT ON FUNCTION einsatz_koordinaten_setzen() IS 'Haelt den PostGIS-Punkt eines Einsatzes mit den Dezimalgraden im Gleichlauf.'; CREATE TRIGGER einsatz_koordinaten BEFORE INSERT OR UPDATE OF "breitengrad", "laengengrad" ON "einsatz" FOR EACH ROW EXECUTE FUNCTION einsatz_koordinaten_setzen(); -- Koordinaten, die es auf der Erde nicht gibt, sind ein Tippfehler. ALTER TABLE "einsatz" ADD CONSTRAINT "einsatz_koordinaten_gueltig" CHECK ( ("breitengrad" IS NULL OR ("breitengrad" >= -90 AND "breitengrad" <= 90)) AND ("laengengrad" IS NULL OR ("laengengrad" >= -180 AND "laengengrad" <= 180)) ); -- ----------------------------------------------------------------------------- -- Freigegebene Einsatzberichte sind unveränderbar -- -- Bis zur Freigabe darf alles nachgetragen werden — ein Einsatz wird tagelang -- ergänzt, und genau dafür ist der Entwurfsstand da. Mit der Freigabe ist -- Schluss: der Bericht ist dann ein Dokument, kein Arbeitsstand. -- -- Zwei Ausnahmen bleiben, beide nachtragend statt verändernd: das PDF, solange -- keines hinterlegt ist, und geaendertAm. -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION einsatz_freigabe_schuetzen() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE vergleich "einsatz"%ROWTYPE; BEGIN IF OLD."status" <> 'FREIGEGEBEN' THEN RETURN NEW; END IF; vergleich := NEW; vergleich."geaendertAm" := OLD."geaendertAm"; IF OLD."berichtDateiId" IS NULL THEN vergleich."berichtDateiId" := OLD."berichtDateiId"; END IF; IF vergleich IS DISTINCT FROM OLD THEN RAISE EXCEPTION 'Der Einsatzbericht % ist freigegeben und damit unveraenderlich.', OLD."einsatznummer" USING ERRCODE = 'insufficient_privilege'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION einsatz_kein_loeschen() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF OLD."status" <> 'FREIGEGEBEN' THEN RETURN OLD; END IF; IF coalesce(current_setting('app.audit_bereinigung', true), '') = 'an' THEN RETURN OLD; END IF; RAISE EXCEPTION 'Der Einsatzbericht % ist freigegeben und darf nicht geloescht werden ' '(nur im Aufbewahrungslauf).', OLD."einsatznummer" USING ERRCODE = 'insufficient_privilege'; END; $$; CREATE TRIGGER einsatz_freigabe BEFORE UPDATE ON "einsatz" FOR EACH ROW EXECUTE FUNCTION einsatz_freigabe_schuetzen(); CREATE TRIGGER einsatz_loeschschutz BEFORE DELETE ON "einsatz" FOR EACH ROW EXECUTE FUNCTION einsatz_kein_loeschen(); -- Dasselbe für die Bestandteile: sonst ließe sich ein freigegebener Bericht -- über die Einsatzkräfte oder den Materialverbrauch nachträglich verändern. CREATE OR REPLACE FUNCTION einsatzinhalt_gesperrt() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE betroffener_einsatz uuid; zustand "EinsatzStatus"; BEGIN IF TG_OP = 'DELETE' THEN betroffener_einsatz := OLD."einsatzId"; ELSE betroffener_einsatz := NEW."einsatzId"; END IF; SELECT "status" INTO zustand FROM "einsatz" WHERE "id" = betroffener_einsatz; IF zustand IS NULL OR zustand <> 'FREIGEGEBEN' THEN IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF; END IF; IF coalesce(current_setting('app.audit_bereinigung', true), '') = 'an' THEN IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF; END IF; RAISE EXCEPTION 'Der Einsatz % ist freigegeben. % auf % ist nicht mehr zulaessig.', betroffener_einsatz, TG_OP, TG_TABLE_NAME USING ERRCODE = 'insufficient_privilege'; END; $$; CREATE TRIGGER einsatzfahrzeug_gesperrt BEFORE INSERT OR UPDATE OR DELETE ON "einsatzfahrzeug" FOR EACH ROW EXECUTE FUNCTION einsatzinhalt_gesperrt(); CREATE TRIGGER einsatzkraft_gesperrt BEFORE INSERT OR UPDATE OR DELETE ON "einsatzkraft" FOR EACH ROW EXECUTE FUNCTION einsatzinhalt_gesperrt(); CREATE TRIGGER materialverbrauch_gesperrt BEFORE INSERT OR UPDATE OR DELETE ON "materialverbrauch" FOR EACH ROW EXECUTE FUNCTION einsatzinhalt_gesperrt(); -- Lagemeldungen bleiben ausdrücklich ausgenommen: sie sind ein Protokoll des -- Verlaufs, kein Bestandteil des Berichts, und eine Meldung aus dem Portal -- kann eintreffen, während der Bericht schon freigegeben ist. -- ----------------------------------------------------------------------------- -- Portalzugänge -- ----------------------------------------------------------------------------- -- Ein Zugang, der nie gültig war, ist ein Fehler in der Erzeugung. ALTER TABLE "portal_zugang" ADD CONSTRAINT "portal_zugang_gueltigkeit" CHECK ( "gueltigBis" > "erstelltAm" ); -- ----------------------------------------------------------------------------- -- Indizes -- ----------------------------------------------------------------------------- -- Der Alarmmonitor fragt immer dasselbe: was läuft gerade. CREATE INDEX "einsatz_laufend" ON "einsatz" ("organisationId", "alarmiertUm" DESC) WHERE "status" = 'LAUFEND' AND "geloeschtAm" IS NULL; -- Räumliche Abfragen ab Phase 6. CREATE INDEX "einsatz_koordinaten_idx" ON "einsatz" USING gist ("koordinaten"); -- Volltextsuche wie in den Phasen davor. CREATE INDEX "einsatz_suche_stichwort" ON "einsatz" USING gin ("stichwort" gin_trgm_ops); CREATE INDEX "einsatz_suche_ort" ON "einsatz" USING gin ("ort" gin_trgm_ops); CREATE INDEX "einsatz_suche_nummer" ON "einsatz" USING gin ("einsatznummer" gin_trgm_ops); CREATE INDEX "uebung_suche_thema" ON "uebung" USING gin ("thema" gin_trgm_ops); -- ----------------------------------------------------------------------------- -- Rechte für die Anwendungsrolle auf die neuen Tabellen. -- ----------------------------------------------------------------------------- DO $$ BEGIN IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'florian_app') THEN EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO florian_app'; EXECUTE 'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO florian_app'; -- Die Einschraenkungen aus den Phasen davor gelten unveraendert weiter. EXECUTE 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app'; ELSE RAISE NOTICE 'Rolle florian_app nicht vorhanden — Rechtevergabe uebersprungen.'; END IF; END; $$;