-- ============================================================================= -- Phase 10 — Mandantenfähigkeit: Absicherung -- -- Zwei Tabellen, zwei sehr verschiedene Schutzbedürfnisse. -- -- `mitgliedschaft` trägt `organisationId` und wird behandelt wie jede andere -- Tabelle: Row Level Security, Policy auf die aktuelle Organisation. Wer in -- Wehr A angemeldet ist, sieht die Mitgliedschaften von Wehr A — und damit -- übrigens auch nicht, in welchen anderen Wehren ein Kollege noch geführt -- wird. Das ist Absicht. -- -- `benutzer` trägt bewusst KEINE `organisationId` und kann es nicht: sie ist -- der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehört. RLS greift hier -- also nicht. Stattdessen wird sie über Rechte geschützt — dieselbe Lösung wie -- bei `berechtigung` aus Phase 0, und aus demselben Grund: eine Tabelle, die -- über allen Mandanten steht, gehört nicht in die Hand der Anwendungsrolle. -- ============================================================================= -- ----------------------------------------------------------------------------- -- Row Level Security für die Mitgliedschaft -- ----------------------------------------------------------------------------- ALTER TABLE "mitgliedschaft" ENABLE ROW LEVEL SECURITY; ALTER TABLE "mitgliedschaft" FORCE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "mitgliedschaft_mandant" ON "mitgliedschaft"; CREATE POLICY "mitgliedschaft_mandant" ON "mitgliedschaft" FOR ALL USING ("organisationId" = aktuelle_organisation()) WITH CHECK ("organisationId" = aktuelle_organisation()); -- ----------------------------------------------------------------------------- -- Prüfbedingungen -- ----------------------------------------------------------------------------- -- Eine Mitgliedschaft als MITGLIED braucht die Personenakte — sie ist ja -- genau die Verbindung zwischen Konto und Wehr. Die Aufsicht darf keine -- haben: die Kreisebene gehört der Wehr nicht an und hat dort nichts in der -- Mitgliederliste zu suchen. ALTER TABLE "mitgliedschaft" ADD CONSTRAINT "mitgliedschaft_akte_passend" CHECK ( ("art" = 'MITGLIED' AND "personId" IS NOT NULL) OR ("art" = 'AUFSICHT' AND "personId" IS NULL) ); -- Die Akte muss zu derselben Wehr gehören wie die Mitgliedschaft. Ein -- Fremdschlüssel allein sagt darüber nichts — er prüft nur, dass es die Akte -- gibt, nicht, wo sie liegt. Ohne diese Bedingung ließe sich ein Konto über -- eine Akte aus Wehr A in Wehr B hineinhängen, und die Sitzung stünde mit -- einer personId da, die dort niemandem gehört. CREATE OR REPLACE FUNCTION mitgliedschaft_akte_gleiche_wehr() RETURNS TRIGGER AS $$ DECLARE akte_wehr uuid; BEGIN IF NEW."personId" IS NULL THEN RETURN NEW; END IF; SELECT "organisationId" INTO akte_wehr FROM "person" WHERE "id" = NEW."personId"; IF akte_wehr IS DISTINCT FROM NEW."organisationId" THEN RAISE EXCEPTION 'Die Personenakte gehoert zu einer anderen Wehr als die Mitgliedschaft. ' 'Ein Konto wird je Wehr an die dortige Akte gehaengt.'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS mitgliedschaft_akte_gleiche_wehr_trigger ON "mitgliedschaft"; CREATE TRIGGER mitgliedschaft_akte_gleiche_wehr_trigger BEFORE INSERT OR UPDATE OF "personId", "organisationId" ON "mitgliedschaft" FOR EACH ROW EXECUTE FUNCTION mitgliedschaft_akte_gleiche_wehr(); -- Eine E-Mail-Adresse ohne @ ist keine. Die Anmeldung hängt daran, und ein -- Konto, mit dem sich niemand anmelden kann, fällt sonst erst auf, wenn es -- jemand versucht. ALTER TABLE "benutzer" ADD CONSTRAINT "benutzer_email_plausibel" CHECK ( "email" LIKE '%@%.%' AND length("email") >= 6 ); -- Ein Konto ohne jeden Anmeldeweg wäre eine Karteileiche. Entweder ein -- Passwort oder ein OIDC-Subject — sonst ist es keines. ALTER TABLE "benutzer" ADD CONSTRAINT "benutzer_anmeldeweg_vorhanden" CHECK ( "passwortHash" IS NOT NULL OR "oidcSubject" IS NOT NULL ); -- ----------------------------------------------------------------------------- -- Rechte -- ----------------------------------------------------------------------------- DO $$ BEGIN IF EXISTS (SELECT 1 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'; EXECUTE 'REVOKE UPDATE, DELETE ON "druckmessung" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "atemschutz_ereignis" FROM florian_app'; EXECUTE 'REVOKE DELETE ON "benachrichtigung" FROM florian_app'; EXECUTE 'REVOKE DELETE ON "ki_vorgang" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "wikiversion" FROM florian_app'; EXECUTE 'REVOKE DELETE ON "kassenabschluss" FROM florian_app'; -- Konten legt die Anwendungsrolle nicht an und löscht sie nicht. -- -- Beides geht über den Systemzugang, also über denselben Weg, über -- den sich schon heute die Anmeldung eine Person sucht (RLS kennt vor -- der Anmeldung ja noch keine Organisation). UPDATE bleibt erlaubt, -- weil jede Anmeldung eines schreibt: letzte Anmeldung, Fehlversuche, -- Sperre. Ein REVOKE darauf hiesse, dass niemand sich mehr anmelden -- kann. EXECUTE 'REVOKE INSERT, DELETE ON "benutzer" FROM florian_app'; END IF; END $$; -- ----------------------------------------------------------------------------- -- Ein Konto behält seine Kennung -- ----------------------------------------------------------------------------- -- Was die Anwendungsrolle am Konto ändern darf, ist genau das, was bei jeder -- Anmeldung anfällt. Alles Übrige — E-Mail, Passwort, Verwalterrecht — -- gehört in die Kontenverwaltung und damit an den Systemzugang. -- -- Ohne diese Sperre wäre `istVerwalter` über jeden Weg erreichbar, der ein -- Konto schreibt. Und ein Merkmal, das die Mandantentrennung aufhebt, darf -- nicht dort gesetzt werden können, wo eine Wehr ihre eigenen Daten pflegt. CREATE OR REPLACE FUNCTION benutzer_kennung_unveraenderlich() RETURNS TRIGGER AS $$ BEGIN IF current_user <> 'florian_app' THEN RETURN NEW; END IF; IF NEW."email" IS DISTINCT FROM OLD."email" OR NEW."istVerwalter" IS DISTINCT FROM OLD."istVerwalter" OR NEW."oidcSubject" IS DISTINCT FROM OLD."oidcSubject" THEN RAISE EXCEPTION 'E-Mail, OIDC-Subject und das Verwalterrecht eines Kontos werden ueber die ' 'Kontenverwaltung geaendert, nicht aus einer Wehr heraus.'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS benutzer_kennung_unveraenderlich_trigger ON "benutzer"; CREATE TRIGGER benutzer_kennung_unveraenderlich_trigger BEFORE UPDATE ON "benutzer" FOR EACH ROW EXECUTE FUNCTION benutzer_kennung_unveraenderlich();