-- ============================================================================= -- Phase 5: Mandantentrennung und die Regeln, die das Protokoll tragen. -- -- Dieses Modul ist sicherheitsrelevant. Was hier steht, ist im Zweifelsfall -- das, was vor der Unfallkasse zaehlt — deshalb liegen die Regeln in der -- Datenbank und nicht im Anwendungscode, wo ein vergessener Zweig genuegt: -- -- 1. Row Level Security fuer die sieben neuen Tabellen. -- -- 2. Das Protokoll ist unveraenderlich. Druckmessungen und Ereignisse -- lassen sich anlegen, aber nicht aendern und nicht loeschen. Ein -- Tippfehler wird durch eine neue Messung richtiggestellt, nicht durch -- Ueberschreiben — sonst waere jede Aufzeichnung nachtraeglich -- glaettbar, und genau das darf sie nicht sein. -- -- 3. Bedingungen, die Unmoegliches abweisen: ein negativer Druck, ein -- Rueckzug vor dem Einstieg, ein Trupp ohne Zeitfolge. -- -- 4. Teilindizes fuer die einzige Abfrage, die im Einsatz im Sekundentakt -- laeuft: welche Trupps sind gerade drin. -- ============================================================================= -- ----------------------------------------------------------------------------- -- Row Level Security -- ----------------------------------------------------------------------------- DO $$ DECLARE tabelle text; BEGIN FOREACH tabelle IN ARRAY ARRAY[ 'atemschutzeinsatz', 'atemschutztrupp', 'truppmitglied', 'druckmessung', 'atemschutz_ereignis', 'g26_untersuchung', 'belastungsuebung' ] 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; $$; -- ----------------------------------------------------------------------------- -- Was es nicht geben darf -- -- Ein Pressluftatmer haelt 300 bar; alles darueber ist ein Tippfehler, alles -- unter null gibt es nicht. Die Zeitfolge eines Trupps ist immer dieselbe: -- angelegt, eingestiegen, Rueckzug, draussen. Sie darf sich nicht verdrehen. -- ----------------------------------------------------------------------------- ALTER TABLE "druckmessung" ADD CONSTRAINT "druckmessung_druck_moeglich" CHECK ( "druck" >= 0 AND "druck" <= 400 ); ALTER TABLE "atemschutztrupp" ADD CONSTRAINT "trupp_einstieg_nach_anlage" CHECK ( "einstiegUm" IS NULL OR "einstiegUm" >= "angelegtUm" ); ALTER TABLE "atemschutztrupp" ADD CONSTRAINT "trupp_rueckzug_nach_einstieg" CHECK ( "rueckzugUm" IS NULL OR ("einstiegUm" IS NOT NULL AND "rueckzugUm" >= "einstiegUm") ); ALTER TABLE "atemschutztrupp" ADD CONSTRAINT "trupp_draussen_nach_einstieg" CHECK ( "drausenUm" IS NULL OR ("einstiegUm" IS NOT NULL AND "drausenUm" >= "einstiegUm") ); ALTER TABLE "atemschutztrupp" ADD CONSTRAINT "trupp_mayday_aufhebung_danach" CHECK ( "maydayAufgehobenUm" IS NULL OR ("maydayUm" IS NOT NULL AND "maydayAufgehobenUm" >= "maydayUm") ); ALTER TABLE "atemschutzeinsatz" ADD CONSTRAINT "atemschutz_ende_nach_beginn" CHECK ( "endeUm" IS NULL OR "endeUm" >= "beginnUm" ); ALTER TABLE "atemschutzeinsatz" ADD CONSTRAINT "atemschutz_intervall_sinnvoll" CHECK ( "abfrageIntervall" BETWEEN 60 AND 3600 ); ALTER TABLE "g26_untersuchung" ADD CONSTRAINT "g26_gueltig_nach_untersuchung" CHECK ( "gueltigBis" > "untersuchtAm" ); -- ----------------------------------------------------------------------------- -- Genau ein Truppfuehrer je Trupp -- -- Zwei Fuehrer sind keine Kleinigkeit: im Protokoll stuende dann nicht mehr, -- wer verantwortlich war. -- ----------------------------------------------------------------------------- CREATE UNIQUE INDEX "truppmitglied_ein_fuehrer" ON "truppmitglied" ("truppId") WHERE "truppfuehrer"; -- ----------------------------------------------------------------------------- -- Teilindizes fuer den laufenden Einsatz -- ----------------------------------------------------------------------------- CREATE INDEX "atemschutz_laufend" ON "atemschutzeinsatz" ("organisationId", "beginnUm" DESC) WHERE "endeUm" IS NULL; CREATE INDEX "trupp_drinnen" ON "atemschutztrupp" ("organisationId", "atemschutzeinsatzId") WHERE "status" IN ('IM_EINSATZ', 'RUECKWEG'); CREATE INDEX "g26_gueltig" ON "g26_untersuchung" ("organisationId", "gueltigBis" DESC) WHERE "geloeschtAm" IS NULL; -- ----------------------------------------------------------------------------- -- Das Protokoll ist unveraenderlich -- -- Zwei Tabellen sind reine Aufzeichnung: was gemessen und was passiert ist. -- Beides wird angelegt und danach nie wieder angefasst. Der Trigger ist die -- zweite Sperre neben dem entzogenen Recht weiter unten — eine davon reicht, -- aber bei einer Aufzeichnung, an der Menschenleben haengen, ist die zweite -- ihren Preis wert. -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION atemschutz_protokoll_unveraenderlich() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'Das Atemschutzprotokoll ist unveraenderlich. Ein Fehler wird durch einen neuen Eintrag richtiggestellt, nicht durch Aendern (Tabelle %).', TG_TABLE_NAME USING ERRCODE = 'check_violation'; END; $$ LANGUAGE plpgsql; CREATE TRIGGER druckmessung_unveraenderlich BEFORE UPDATE OR DELETE ON "druckmessung" FOR EACH ROW EXECUTE FUNCTION atemschutz_protokoll_unveraenderlich(); CREATE TRIGGER atemschutz_ereignis_unveraenderlich BEFORE UPDATE OR DELETE ON "atemschutz_ereignis" FOR EACH ROW EXECUTE FUNCTION atemschutz_protokoll_unveraenderlich(); -- ----------------------------------------------------------------------------- -- Rechte fuer die Anwendungsrolle -- ----------------------------------------------------------------------------- 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'; EXECUTE 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "kiosk_vorgang" FROM florian_app'; -- Aufzeichnung, kein Datenbestand: anlegen ja, aendern nein. EXECUTE 'REVOKE UPDATE, DELETE ON "druckmessung" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "atemschutz_ereignis" FROM florian_app'; ELSE RAISE NOTICE 'Rolle florian_app nicht vorhanden — Rechtevergabe uebersprungen.'; END IF; END; $$;