-- CreateEnum CREATE TYPE "Rolle" AS ENUM ('ADMIN', 'OPERATOR', 'LESER'); -- CreateEnum CREATE TYPE "SchluesselTyp" AS ENUM ('ED25519', 'RSA4096', 'ANDERER'); -- CreateEnum CREATE TYPE "SchluesselKategorie" AS ENUM ('PERSON', 'DIENST', 'DEPLOYMENT'); -- CreateEnum CREATE TYPE "SchluesselStatus" AS ENUM ('AKTIV', 'ROTIERT', 'WIDERRUFEN', 'ABGELAUFEN'); -- CreateEnum CREATE TYPE "Anmeldeart" AS ENUM ('DIENSTBENUTZER', 'ROOT'); -- CreateEnum CREATE TYPE "DriftZustand" AS ENUM ('UNBEKANNT', 'SYNCHRON', 'DRIFT'); -- CreateEnum CREATE TYPE "HostSchluesselStatus" AS ENUM ('AUSSTEHEND', 'VERTRAUT', 'ABGELEHNT', 'ERSETZT'); -- CreateEnum CREATE TYPE "VerwendungsZustand" AS ENUM ('GEPLANT', 'AUSGEROLLT', 'ABWEICHUNG', 'ENTFERNEN', 'ENTFERNT', 'FEHLGESCHLAGEN'); -- CreateEnum CREATE TYPE "WiderrufArt" AS ENUM ('LOESCHEN', 'UMBENENNEN'); -- CreateEnum CREATE TYPE "SudoModus" AS ENUM ('KEINER', 'BEFEHLE', 'VOLL'); -- CreateEnum CREATE TYPE "AbweichungsArt" AS ENUM ('UNBEKANNTER_SCHLUESSEL', 'FEHLENDER_SCHLUESSEL', 'ZUSAETZLICH_IM_BLOCK', 'DATEI_GEAENDERT', 'RECHTE', 'HOSTKEY_GEAENDERT', 'PRIVATE_KEY_FEHLT'); -- CreateEnum CREATE TYPE "VorgangsArt" AS ENUM ('HOSTKEY_ABRUFEN', 'ERREICHBARKEIT', 'BOOTSTRAP', 'TROCKENLAUF', 'AUSROLLEN', 'ABGLEICH', 'VERBINDUNGSTEST', 'ROTATION', 'WIDERRUF', 'PRIVATE_KEY_ABHOLEN'); -- CreateEnum CREATE TYPE "VorgangsStatus" AS ENUM ('WARTEND', 'LAEUFT', 'WARTET_AUF_BESTAETIGUNG', 'ERLEDIGT', 'FEHLGESCHLAGEN', 'ABGEBROCHEN'); -- CreateEnum CREATE TYPE "ProtokollErgebnis" AS ENUM ('ERFOLG', 'FEHLER', 'VERWEIGERT'); -- CreateTable CREATE TABLE "Benutzer" ( "id" TEXT NOT NULL, "oidcSub" TEXT, "email" TEXT, "anmeldename" TEXT, "name" TEXT NOT NULL, "rolle" "Rolle" NOT NULL DEFAULT 'LESER', "aktiv" BOOLEAN NOT NULL DEFAULT true, "istNotfall" BOOLEAN NOT NULL DEFAULT false, "passwortHash" TEXT, "totpGeheimnis" BYTEA, "totpEingerichtetAm" TIMESTAMP(3), "totpLetzterSchritt" BIGINT, "sitzungenGueltigAb" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "bestaetigtAm" TIMESTAMP(3), "letzteAnmeldungAm" TIMESTAMP(3), "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "geaendertAm" TIMESTAMP(3) NOT NULL, CONSTRAINT "Benutzer_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Person" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "email" TEXT, "organisation" TEXT, "notiz" TEXT, "benutzerId" TEXT, "archiviertAm" TIMESTAMP(3), "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "Person_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Kunde" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, CONSTRAINT "Kunde_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Host" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "adresse" TEXT NOT NULL, "port" INTEGER NOT NULL DEFAULT 22, "beschreibung" TEXT, "tags" TEXT[], "kundeId" TEXT, "anmeldeart" "Anmeldeart" NOT NULL DEFAULT 'DIENSTBENUTZER', "deploymentSchluesselId" TEXT, "bootstrapAm" TIMESTAMP(3), "erreichbar" BOOLEAN, "zuletztGeprueftAm" TIMESTAMP(3), "letzterAbgleichAm" TIMESTAMP(3), "driftZustand" "DriftZustand" NOT NULL DEFAULT 'UNBEKANNT', "sshdBefund" JSONB, "archiviertAm" TIMESTAMP(3), "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "geaendertAm" TIMESTAMP(3) NOT NULL, CONSTRAINT "Host_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Hostgruppe" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "beschreibung" TEXT, "deploymentSchluesselId" TEXT, "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "Hostgruppe_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "HostgruppeMitglied" ( "hostId" TEXT NOT NULL, "hostgruppeId" TEXT NOT NULL, CONSTRAINT "HostgruppeMitglied_pkey" PRIMARY KEY ("hostId","hostgruppeId") ); -- CreateTable CREATE TABLE "HostSchluessel" ( "id" TEXT NOT NULL, "hostId" TEXT NOT NULL, "algorithmus" TEXT NOT NULL, "publicKey" TEXT NOT NULL, "fingerprint" TEXT NOT NULL, "status" "HostSchluesselStatus" NOT NULL DEFAULT 'AUSSTEHEND', "erstmalsGesehen" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "zuletztGesehen" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "entschiedenVon" TEXT, "entschiedenAm" TIMESTAMP(3), CONSTRAINT "HostSchluessel_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Schluessel" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "slug" TEXT NOT NULL, "typ" "SchluesselTyp" NOT NULL, "kategorie" "SchluesselKategorie" NOT NULL, "status" "SchluesselStatus" NOT NULL DEFAULT 'AKTIV', "publicKey" TEXT NOT NULL, "fingerprint" TEXT NOT NULL, "kommentar" TEXT NOT NULL, "privatVerschluesselt" BYTEA, "dekVerschluesselt" BYTEA, "masterKeyVersion" INTEGER, "privatGeloeschtAm" TIMESTAMP(3), "passphraseGeschuetzt" BOOLEAN NOT NULL DEFAULT false, "instanzStandard" BOOLEAN NOT NULL DEFAULT false, "personId" TEXT, "vorlageId" TEXT, "tags" TEXT[], "ablaufAm" TIMESTAMP(3), "zuletztVerwendetAm" TIMESTAMP(3), "vorgaengerId" TEXT, "erstelltVonId" TEXT, "selbstHinterlegt" BOOLEAN NOT NULL DEFAULT false, "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "geaendertAm" TIMESTAMP(3) NOT NULL, CONSTRAINT "Schluessel_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "PublicVerwendung" ( "id" TEXT NOT NULL, "schluesselId" TEXT NOT NULL, "hostId" TEXT, "hostgruppeId" TEXT, "zielbenutzer" TEXT NOT NULL, "optionen" TEXT, "zustand" "VerwendungsZustand" NOT NULL DEFAULT 'GEPLANT', "zuletztAngewendetAm" TIMESTAMP(3), "angelegtVonId" TEXT, "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "PublicVerwendung_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "PrivateVerwendung" ( "id" TEXT NOT NULL, "schluesselId" TEXT NOT NULL, "hostId" TEXT NOT NULL, "zielbenutzer" TEXT NOT NULL, "pfad" TEXT NOT NULL, "sshConfig" JSONB, "knownHostsFuer" TEXT[], "beimWiderruf" "WiderrufArt" NOT NULL DEFAULT 'LOESCHEN', "zustand" "VerwendungsZustand" NOT NULL DEFAULT 'GEPLANT', "zuletztAngewendetAm" TIMESTAMP(3), "angelegtVonId" TEXT, "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "PrivateVerwendung_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Zielkonto" ( "id" TEXT NOT NULL, "hostId" TEXT NOT NULL, "benutzername" TEXT NOT NULL, "vonSshkvAngelegt" BOOLEAN NOT NULL DEFAULT true, "sudoModus" "SudoModus" NOT NULL DEFAULT 'KEINER', "sudoBefehle" TEXT[], "sudoersDatei" TEXT, "sudoersInhalt" TEXT, "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "entferntAm" TIMESTAMP(3), CONSTRAINT "Zielkonto_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Vorlage" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "beschreibung" TEXT, "eingebaut" BOOLEAN NOT NULL DEFAULT false, "schluesselTyp" "SchluesselTyp" NOT NULL DEFAULT 'ED25519', "kategorie" "SchluesselKategorie" NOT NULL DEFAULT 'DIENST', "standardLaufzeitTage" INTEGER, "zielbenutzer" TEXT NOT NULL, "zielbenutzerAnlegen" BOOLEAN NOT NULL DEFAULT false, "sudoModus" "SudoModus" NOT NULL DEFAULT 'KEINER', "sudoBefehle" TEXT[], "authorizedKeysOptionen" TEXT, "sshConfigAlias" TEXT, "knownHostsEintragen" BOOLEAN NOT NULL DEFAULT true, "privatNachAusrollenLoeschen" BOOLEAN NOT NULL DEFAULT false, "archiviertAm" TIMESTAMP(3), "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "Vorlage_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Beobachtung" ( "id" TEXT NOT NULL, "hostId" TEXT NOT NULL, "zielbenutzer" TEXT NOT NULL, "datei" TEXT NOT NULL, "sha256" TEXT NOT NULL, "eintraege" JSONB NOT NULL, "beobachtetAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "Beobachtung_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Abweichung" ( "id" TEXT NOT NULL, "hostId" TEXT NOT NULL, "zielbenutzer" TEXT, "art" "AbweichungsArt" NOT NULL, "fingerprint" TEXT, "details" JSONB, "eroeffnetAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "behobenAm" TIMESTAMP(3), CONSTRAINT "Abweichung_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Vorgang" ( "id" TEXT NOT NULL, "art" "VorgangsArt" NOT NULL, "status" "VorgangsStatus" NOT NULL DEFAULT 'WARTEND', "angefordertVonId" TEXT, "hostId" TEXT, "schluesselId" TEXT, "eingabe" JSONB, "plan" JSONB, "ergebnis" JSONB, "fehler" TEXT, "jobId" TEXT, "angelegtAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "gestartetAm" TIMESTAMP(3), "beendetAm" TIMESTAMP(3), CONSTRAINT "Vorgang_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Protokoll" ( "id" BIGSERIAL NOT NULL, "zeitpunkt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "akteurId" TEXT, "akteurName" TEXT NOT NULL, "aktion" TEXT NOT NULL, "hostName" TEXT, "zielbenutzer" TEXT, "fingerprint" TEXT, "diff" TEXT, "ergebnis" "ProtokollErgebnis" NOT NULL, "details" JSONB, "vorgaengerHash" TEXT NOT NULL DEFAULT '', "hash" TEXT NOT NULL DEFAULT '', CONSTRAINT "Protokoll_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Einstellung" ( "schluessel" TEXT NOT NULL, "wert" JSONB NOT NULL, "geaendertAm" TIMESTAMP(3) NOT NULL, CONSTRAINT "Einstellung_pkey" PRIMARY KEY ("schluessel") ); -- CreateTable CREATE TABLE "Ratenbegrenzung" ( "schluessel" TEXT NOT NULL, "fensterStart" TIMESTAMP(3) NOT NULL, "anzahl" INTEGER NOT NULL, CONSTRAINT "Ratenbegrenzung_pkey" PRIMARY KEY ("schluessel") ); -- CreateIndex CREATE UNIQUE INDEX "Benutzer_oidcSub_key" ON "Benutzer"("oidcSub"); -- CreateIndex CREATE UNIQUE INDEX "Benutzer_email_key" ON "Benutzer"("email"); -- CreateIndex CREATE UNIQUE INDEX "Benutzer_anmeldename_key" ON "Benutzer"("anmeldename"); -- CreateIndex CREATE UNIQUE INDEX "Person_benutzerId_key" ON "Person"("benutzerId"); -- CreateIndex CREATE UNIQUE INDEX "Kunde_name_key" ON "Kunde"("name"); -- CreateIndex CREATE UNIQUE INDEX "Host_name_key" ON "Host"("name"); -- CreateIndex CREATE UNIQUE INDEX "Hostgruppe_name_key" ON "Hostgruppe"("name"); -- CreateIndex CREATE UNIQUE INDEX "HostSchluessel_hostId_fingerprint_key" ON "HostSchluessel"("hostId", "fingerprint"); -- CreateIndex CREATE UNIQUE INDEX "Schluessel_slug_key" ON "Schluessel"("slug"); -- CreateIndex CREATE UNIQUE INDEX "Schluessel_fingerprint_key" ON "Schluessel"("fingerprint"); -- CreateIndex CREATE UNIQUE INDEX "Schluessel_vorgaengerId_key" ON "Schluessel"("vorgaengerId"); -- CreateIndex CREATE UNIQUE INDEX "Zielkonto_hostId_benutzername_key" ON "Zielkonto"("hostId", "benutzername"); -- CreateIndex CREATE UNIQUE INDEX "Vorlage_name_key" ON "Vorlage"("name"); -- CreateIndex CREATE UNIQUE INDEX "Beobachtung_hostId_zielbenutzer_datei_key" ON "Beobachtung"("hostId", "zielbenutzer", "datei"); -- CreateIndex CREATE INDEX "Abweichung_hostId_behobenAm_idx" ON "Abweichung"("hostId", "behobenAm"); -- CreateIndex CREATE INDEX "Vorgang_status_angelegtAm_idx" ON "Vorgang"("status", "angelegtAm"); -- CreateIndex CREATE INDEX "Protokoll_zeitpunkt_idx" ON "Protokoll"("zeitpunkt"); -- CreateIndex CREATE INDEX "Protokoll_aktion_idx" ON "Protokoll"("aktion"); -- AddForeignKey ALTER TABLE "Person" ADD CONSTRAINT "Person_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "Benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Host" ADD CONSTRAINT "Host_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Host" ADD CONSTRAINT "Host_deploymentSchluesselId_fkey" FOREIGN KEY ("deploymentSchluesselId") REFERENCES "Schluessel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Hostgruppe" ADD CONSTRAINT "Hostgruppe_deploymentSchluesselId_fkey" FOREIGN KEY ("deploymentSchluesselId") REFERENCES "Schluessel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "HostgruppeMitglied" ADD CONSTRAINT "HostgruppeMitglied_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "HostgruppeMitglied" ADD CONSTRAINT "HostgruppeMitglied_hostgruppeId_fkey" FOREIGN KEY ("hostgruppeId") REFERENCES "Hostgruppe"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "HostSchluessel" ADD CONSTRAINT "HostSchluessel_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Schluessel" ADD CONSTRAINT "Schluessel_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Schluessel" ADD CONSTRAINT "Schluessel_vorlageId_fkey" FOREIGN KEY ("vorlageId") REFERENCES "Vorlage"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Schluessel" ADD CONSTRAINT "Schluessel_vorgaengerId_fkey" FOREIGN KEY ("vorgaengerId") REFERENCES "Schluessel"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Schluessel" ADD CONSTRAINT "Schluessel_erstelltVonId_fkey" FOREIGN KEY ("erstelltVonId") REFERENCES "Benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "PublicVerwendung" ADD CONSTRAINT "PublicVerwendung_schluesselId_fkey" FOREIGN KEY ("schluesselId") REFERENCES "Schluessel"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "PublicVerwendung" ADD CONSTRAINT "PublicVerwendung_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "PublicVerwendung" ADD CONSTRAINT "PublicVerwendung_hostgruppeId_fkey" FOREIGN KEY ("hostgruppeId") REFERENCES "Hostgruppe"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "PrivateVerwendung" ADD CONSTRAINT "PrivateVerwendung_schluesselId_fkey" FOREIGN KEY ("schluesselId") REFERENCES "Schluessel"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "PrivateVerwendung" ADD CONSTRAINT "PrivateVerwendung_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Zielkonto" ADD CONSTRAINT "Zielkonto_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Beobachtung" ADD CONSTRAINT "Beobachtung_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Abweichung" ADD CONSTRAINT "Abweichung_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Vorgang" ADD CONSTRAINT "Vorgang_angefordertVonId_fkey" FOREIGN KEY ("angefordertVonId") REFERENCES "Benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Vorgang" ADD CONSTRAINT "Vorgang_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Vorgang" ADD CONSTRAINT "Vorgang_schluesselId_fkey" FOREIGN KEY ("schluesselId") REFERENCES "Schluessel"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- ===================================================================================== -- Datenbankregeln (#003, #005). Sie gelten auch, wenn jemand an der Service-Schicht -- vorbei schreibt. tests/datenbankregeln.test.ts zaehlt sie namentlich auf. -- ===================================================================================== -- Private Key, Data-Key und Master-Key-Version gibt es nur zusammen. ALTER TABLE "Schluessel" ADD CONSTRAINT "schluessel_umschlag_vollstaendig" CHECK ( ("privatVerschluesselt" IS NULL AND "dekVerschluesselt" IS NULL AND "masterKeyVersion" IS NULL) OR ("privatVerschluesselt" IS NOT NULL AND "dekVerschluesselt" IS NOT NULL AND "masterKeyVersion" IS NOT NULL) ); -- Private Keys von Personen werden nie gespeichert (Plan, Abschnitt Schluessel). ALTER TABLE "Schluessel" ADD CONSTRAINT "schluessel_person_ohne_private_key" CHECK ( "kategorie" <> 'PERSON' OR "privatVerschluesselt" IS NULL ); -- Nur ein Deployment-Key kann Instanz-Standard sein, und davon nur einer zugleich. ALTER TABLE "Schluessel" ADD CONSTRAINT "schluessel_instanzstandard_nur_deployment" CHECK ( NOT "instanzStandard" OR "kategorie" = 'DEPLOYMENT' ); CREATE UNIQUE INDEX "schluessel_ein_instanzstandard" ON "Schluessel" ("instanzStandard") WHERE "instanzStandard" AND "status" = 'AKTIV'; -- Zielbenutzer und Adressen landen in Shell-Skripten auf den Hosts. Was hier nicht passt, -- kommt gar nicht erst bis zur Quotierung. ALTER TABLE "PublicVerwendung" ADD CONSTRAINT "publicverwendung_zielbenutzer_gueltig" CHECK ( "zielbenutzer" ~ '^[a-z_][a-z0-9_-]{0,31}$' ); ALTER TABLE "PrivateVerwendung" ADD CONSTRAINT "privateverwendung_zielbenutzer_gueltig" CHECK ( "zielbenutzer" ~ '^[a-z_][a-z0-9_-]{0,31}$' ); ALTER TABLE "Zielkonto" ADD CONSTRAINT "zielkonto_benutzername_gueltig" CHECK ( "benutzername" ~ '^[a-z_][a-z0-9_-]{0,31}$' ); ALTER TABLE "Host" ADD CONSTRAINT "host_adresse_gueltig" CHECK ( "adresse" ~ '^[A-Za-z0-9._:-]{1,253}$' ); ALTER TABLE "Host" ADD CONSTRAINT "host_port_gueltig" CHECK ("port" BETWEEN 1 AND 65535); -- Pfad eines Private Keys: absolut oder ~/, ohne Steuerzeichen und ohne '..'-Segmente. ALTER TABLE "PrivateVerwendung" ADD CONSTRAINT "privateverwendung_pfad_gueltig" CHECK ( "pfad" ~ '^(~/|/)[A-Za-z0-9._/@+-]+$' AND "pfad" !~ '(^|/)\.\.(/|$)' ); -- Eine Verwendung zielt entweder auf einen Host oder auf eine Hostgruppe. ALTER TABLE "PublicVerwendung" ADD CONSTRAINT "publicverwendung_host_oder_gruppe" CHECK ( ("hostId" IS NULL) <> ("hostgruppeId" IS NULL) ); CREATE UNIQUE INDEX "publicverwendung_eindeutig_host" ON "PublicVerwendung" ("schluesselId", "hostId", "zielbenutzer") WHERE "hostId" IS NOT NULL AND "zustand" <> 'ENTFERNT'; CREATE UNIQUE INDEX "publicverwendung_eindeutig_gruppe" ON "PublicVerwendung" ("schluesselId", "hostgruppeId", "zielbenutzer") WHERE "hostgruppeId" IS NOT NULL AND "zustand" <> 'ENTFERNT'; CREATE UNIQUE INDEX "privateverwendung_eindeutig" ON "PrivateVerwendung" ("hostId", "zielbenutzer", "pfad") WHERE "zustand" <> 'ENTFERNT'; -- Je Host und Algorithmus hoechstens ein vertrauter Host-Key. Ein zweiter waere ein -- geaenderter Host-Key, der nie still neben dem alten stehen darf. CREATE UNIQUE INDEX "hostschluessel_ein_vertrauter" ON "HostSchluessel" ("hostId", "algorithmus") WHERE "status" = 'VERTRAUT'; -- Der Notfall-Admin meldet sich mit Anmeldename und Passwort an. ALTER TABLE "Benutzer" ADD CONSTRAINT "benutzer_notfall_vollstaendig" CHECK ( NOT "istNotfall" OR ("anmeldename" IS NOT NULL AND "passwortHash" IS NOT NULL) ); -- ===================================================================================== -- Protokoll: append-only mit Hash-Kette (#005) -- ===================================================================================== -- Die Hash-Eingabe als eigene Funktion, damit Einfuegen und Pruefen garantiert dasselbe rechnen. CREATE FUNCTION protokoll_hash_eingabe(z "Protokoll") RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT concat_ws(E'\x1f', z."vorgaengerHash", z."id"::text, to_char(z."zeitpunkt", 'YYYY-MM-DD"T"HH24:MI:SS.MS'), coalesce(z."akteurId", ''), z."akteurName", z."aktion", coalesce(z."hostName", ''), coalesce(z."zielbenutzer", ''), coalesce(z."fingerprint", ''), coalesce(z."diff", ''), z."ergebnis"::text, coalesce(z."details"::text, '') ) $$; CREATE FUNCTION protokoll_verketten() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE letzter text; BEGIN -- Serialisiert alle Einfuegungen. Die ID wird erst unter der Sperre vergeben: sonst -- koennte eine spaetere Transaktion mit kleinerer ID nach einer frueheren verketten, -- und die Kette liesse sich in ID-Reihenfolge nicht mehr pruefen. PERFORM pg_advisory_xact_lock(hashtext('sshkv_protokoll')); NEW."id" := nextval(pg_get_serial_sequence('"Protokoll"', 'id')); SELECT "hash" INTO letzter FROM "Protokoll" ORDER BY "id" DESC LIMIT 1; NEW."vorgaengerHash" := coalesce(letzter, ''); NEW."hash" := encode(sha256(convert_to(protokoll_hash_eingabe(NEW), 'UTF8')), 'hex'); RETURN NEW; END; $$; CREATE TRIGGER "protokoll_verketten" BEFORE INSERT ON "Protokoll" FOR EACH ROW EXECUTE FUNCTION protokoll_verketten(); CREATE FUNCTION protokoll_unveraenderlich() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN -- Nur die Testumgebung darf leeren; die Anwendung setzt diese Einstellung nie. IF TG_OP = 'TRUNCATE' AND current_setting('sshkv.test_modus', true) = 'ja' THEN RETURN NULL; END IF; RAISE EXCEPTION 'Das Protokoll ist unveränderlich (% verweigert).', TG_OP USING ERRCODE = 'insufficient_privilege'; END; $$; CREATE TRIGGER "protokoll_kein_update" BEFORE UPDATE ON "Protokoll" FOR EACH ROW EXECUTE FUNCTION protokoll_unveraenderlich(); CREATE TRIGGER "protokoll_kein_delete" BEFORE DELETE ON "Protokoll" FOR EACH ROW EXECUTE FUNCTION protokoll_unveraenderlich(); CREATE TRIGGER "protokoll_kein_truncate" BEFORE TRUNCATE ON "Protokoll" FOR EACH STATEMENT EXECUTE FUNCTION protokoll_unveraenderlich(); -- Liefert die ID der ersten Zeile, deren Hash oder Verkettung nicht stimmt – oder NULL. CREATE FUNCTION protokoll_kette_pruefen() RETURNS bigint LANGUAGE plpgsql STABLE AS $$ DECLARE zeile "Protokoll"; erwartet text := ''; BEGIN FOR zeile IN SELECT * FROM "Protokoll" ORDER BY "id" LOOP IF zeile."vorgaengerHash" <> erwartet OR zeile."hash" <> encode(sha256(convert_to(protokoll_hash_eingabe(zeile), 'UTF8')), 'hex') THEN RETURN zeile."id"; END IF; erwartet := zeile."hash"; END LOOP; RETURN NULL; END; $$;