diff --git a/prisma/migrations/20260824074643_sicherheitsaudit/migration.sql b/prisma/migrations/20260824074643_sicherheitsaudit/migration.sql new file mode 100644 index 0000000..88826c6 --- /dev/null +++ b/prisma/migrations/20260824074643_sicherheitsaudit/migration.sql @@ -0,0 +1,31 @@ +-- sicherheitsaudit +-- +-- Erzeugt mit "npm run db:migration". Entfernt wurden die +-- DROP-INDEX-Anweisungen zu diesen Indizes: +-- dokument_absender_trgm +-- vorgang_titel_trgm +-- +-- Sie stammen aus prisma/sql/volltextsuche.sql und sind Prisma unbekannt; +-- ohne sie waere die Volltextsuche still langsam geworden. + +-- AlterTable +ALTER TABLE "benutzer" ADD COLUMN "totpLetzterSchritt" INTEGER; + +-- CreateTable +CREATE TABLE "ersteinrichtung" ( + "id" TEXT NOT NULL, + "abgeschlossenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT, + + CONSTRAINT "ersteinrichtung_pkey" PRIMARY KEY ("id") +); + +-- Bestehende Installationen nachziehen. +-- +-- Die Tabelle oben ist die Sperre fuer die Ersteinrichtung. Auf einem +-- System, das schon laeuft, ist die Einrichtung laengst erledigt - ohne +-- diese Zeile stuende die Sperre nach dem Update offen, und der Weg zum +-- Anlegen eines Administrators waere wieder frei. +INSERT INTO "ersteinrichtung" ("id", "userId") +SELECT 'einmalig', (SELECT "id" FROM "benutzer" ORDER BY "erstelltAm" ASC LIMIT 1) +WHERE EXISTS (SELECT 1 FROM "benutzer"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d8dfb5c..13577ae 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -70,6 +70,13 @@ model User { // TOTP ist Pflicht. Das Secret liegt verschluesselt, nicht im Klartext. totpSecretVerschluesselt String? totpAktiviertAm DateTime? + /// Der zuletzt akzeptierte TOTP-Zeitschritt (Unixzeit / 30). + /// + /// Ein TOTP-Code gilt 30 Sekunden, mit Toleranz bis zu 90. In dieser Zeit + /// liesse sich derselbe Code ein zweites Mal einloesen - abgelesen ueber + /// die Schulter oder abgefangen. Deshalb wird der Schritt festgehalten + /// und nur noch ein groesserer akzeptiert. + totpLetzterSchritt Int? // Einmal-Wiederherstellungscodes, einzeln Argon2id-gehasht. wiederherstellungscodes String[] @default([]) @@ -128,6 +135,25 @@ model BetreuungsZugang { @@map("betreuungs_zugang") } +/// Haelt fest, dass die Ersteinrichtung stattgefunden hat. +/// +/// Die Tabelle hat genau eine Zeile mit fester Id. Das ist kein Protokoll, +/// sondern eine Sperre: Vorher stand im Code "wenn es noch keinen Benutzer +/// gibt, lege einen an" - zwei Anfragen gleichzeitig kamen beide durch die +/// Pruefung und legten beide einen Administrator an. Ueber den +/// Primaerschluessel entscheidet das jetzt die Datenbank, und zwar fuer +/// genau einen Gewinner. +model Ersteinrichtung { + /// Immer "einmalig". Der feste Wert ist die Sperre. + id String @id + + abgeschlossenAm DateTime @default(now()) + /// Wer den Zugang angelegt hat - nur zum Nachvollziehen. + userId String? + + @@map("ersteinrichtung") +} + /// Fehlgeschlagene und erfolgreiche Anmeldeversuche - Grundlage fuer das /// Rate-Limiting am Login. Wird nach 30 Tagen aufgeraeumt. model LoginVersuch {