diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a299e..84b7bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Audit-Log-Migration mit append-only Triggern und Tenant-Zeit-Index angelegt. - VMID-Allokator-Migration mit transaktionssicherer Reservation pro Cluster angelegt. - VMs-und-SSH-Keys-Migration mit Ressourcen-Mapping und eindeutigem Proxmox-VMID-Index angelegt. - Cluster-Migration fuer Proxmox-Verbindungen mit `encrypted_token bytea` angelegt. diff --git a/TODO.md b/TODO.md index 20c9bdc..8f5b7cc 100644 --- a/TODO.md +++ b/TODO.md @@ -59,6 +59,11 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda - [x] VMID-Reservationen vor Proxmox-Calls modelliert - [x] Transaktionssichere Funktion `reserve_next_vmid(cluster_id)` mit `SELECT ... FOR UPDATE` angelegt - [x] Concurrency-Test mit 50 parallelen Reservierungen lokal verifiziert +- [x] E2-T07: Audit-Log-Tabelle + - [x] Append-only Tabelle `audit_log` angelegt + - [x] Tenant- und Profil-Referenzen gesetzt + - [x] Index auf `(tenant_id, created_at)` gesetzt + - [x] UPDATE/DELETE per Trigger blockiert ## MVP-Backlog @@ -103,3 +108,4 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda - 2026-06-10: Cluster-Migration `0004_clusters` angelegt und lokal gegen Supabase verifiziert. - 2026-06-10: VMs-und-SSH-Keys-Migration `0005_vms_and_ssh_keys` angelegt und lokal gegen Supabase verifiziert. - 2026-06-10: VMID-Allokator-Migration `0006_vmid_allocator` angelegt und mit 50 parallelen Reservierungen lokal gegen Supabase verifiziert. +- 2026-06-10: Audit-Log-Migration `0007_audit_log` angelegt und Append-only-Verhalten lokal gegen Supabase verifiziert. diff --git a/migrations/0007_audit_log.down.sql b/migrations/0007_audit_log.down.sql new file mode 100644 index 0000000..221ed20 --- /dev/null +++ b/migrations/0007_audit_log.down.sql @@ -0,0 +1,7 @@ +DROP TRIGGER IF EXISTS audit_log_no_delete ON public.audit_log; +DROP TRIGGER IF EXISTS audit_log_no_update ON public.audit_log; +DROP FUNCTION IF EXISTS public.prevent_audit_log_mutation(); +DROP INDEX IF EXISTS public.audit_log_target_idx; +DROP INDEX IF EXISTS public.audit_log_profile_id_idx; +DROP INDEX IF EXISTS public.audit_log_tenant_id_created_at_idx; +DROP TABLE IF EXISTS public.audit_log; diff --git a/migrations/0007_audit_log.up.sql b/migrations/0007_audit_log.up.sql new file mode 100644 index 0000000..288f2b4 --- /dev/null +++ b/migrations/0007_audit_log.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE public.audit_log ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE RESTRICT, + profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL, + action text NOT NULL, + target_type text NOT NULL, + target_id uuid, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT audit_log_action_not_blank_check CHECK (btrim(action) <> ''), + CONSTRAINT audit_log_target_type_not_blank_check CHECK (btrim(target_type) <> '') +); + +CREATE INDEX audit_log_tenant_id_created_at_idx ON public.audit_log (tenant_id, created_at DESC); +CREATE INDEX audit_log_profile_id_idx ON public.audit_log (profile_id); +CREATE INDEX audit_log_target_idx ON public.audit_log (target_type, target_id); + +CREATE OR REPLACE FUNCTION public.prevent_audit_log_mutation() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION 'audit_log is append-only'; +END; +$$; + +CREATE TRIGGER audit_log_no_update + BEFORE UPDATE ON public.audit_log + FOR EACH ROW + EXECUTE FUNCTION public.prevent_audit_log_mutation(); + +CREATE TRIGGER audit_log_no_delete + BEFORE DELETE ON public.audit_log + FOR EACH ROW + EXECUTE FUNCTION public.prevent_audit_log_mutation();