154 lines
7.0 KiB
PL/PgSQL
154 lines
7.0 KiB
PL/PgSQL
-- =============================================================================
|
|
-- 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();
|