feat(datenbank): Datenmodell mit Datenbankregeln und Seed (#003)
Alle Modelle fuer die fuenf Phasen in einer Migration, damit spaetere Phasen keine Umbauten an Kerntabellen brauchen. Was auch an der Service-Schicht vorbei gelten muss, steht als CHECK, partieller Index oder Trigger in der Datenbank: Personen-Keys ohne Private Key, genau ein Instanz-Deployment-Key, ein vertrauter Host-Key je Algorithmus, shell-sichere Zielbenutzer und Pfade. Das Protokoll ist append-only und per Hash-Kette verbunden; den Hash berechnet ein Trigger, damit keine Zeile ohne Verkettung entstehen kann (#005 baut darauf auf). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,598 @@
|
||||
-- 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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user