-- ITSM-Plattform: PostgreSQL-Schema -- Mandantenfaehig (mehrere Organisationen auf einer Instanz, Daten per tenant_id getrennt). -- Enthaelt audit_log fuer Nachvollziehbarkeit gemaess ISO 27001 A.12.4 / DSGVO Art. 30. CREATE TABLE IF NOT EXISTS tenants ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), dsb_name TEXT, -- Datenschutzbeauftragter/Kontakt dsb_email TEXT, retention_tickets_days INTEGER NOT NULL DEFAULT 1095, -- DSGVO Speicherbegrenzung (Default ~3 Jahre) retention_audit_days INTEGER NOT NULL DEFAULT 1825, -- Audit-Log-Aufbewahrung (Default ~5 Jahre) sla_antwort_minuten INTEGER NOT NULL DEFAULT 480, sla_loesung_minuten INTEGER NOT NULL DEFAULT 2880 ); CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'agent', -- 'admin' | 'agent' active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_login_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS services ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, beschreibung TEXT, kategorie TEXT, gebucht BOOLEAN NOT NULL DEFAULT TRUE, endpoint TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS tickets ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, ticket_nr TEXT NOT NULL, titel TEXT NOT NULL, beschreibung TEXT, service_id INTEGER REFERENCES services(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'Offen', prioritaet TEXT NOT NULL DEFAULT 'Mittel', kategorie TEXT NOT NULL DEFAULT 'Task', zugewiesen_an TEXT, ersteller_id INTEGER REFERENCES users(id) ON DELETE SET NULL, fortschritt INTEGER NOT NULL DEFAULT 0, sla_antwort_minuten INTEGER, sla_loesung_minuten INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, ticket_nr) ); CREATE TABLE IF NOT EXISTS ticket_timeline ( id SERIAL PRIMARY KEY, ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, tenant_id INTEGER NOT NULL, zeit TIMESTAMPTZ NOT NULL DEFAULT now(), akteur TEXT, text TEXT ); CREATE TABLE IF NOT EXISTS audit_log ( id SERIAL PRIMARY KEY, tenant_id INTEGER REFERENCES tenants(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, zeit TIMESTAMPTZ NOT NULL DEFAULT now(), aktion TEXT NOT NULL, entity_typ TEXT, entity_id TEXT, details JSONB, ip TEXT ); CREATE INDEX IF NOT EXISTS idx_tickets_tenant_status ON tickets (tenant_id, status); CREATE INDEX IF NOT EXISTS idx_tickets_tenant_kategorie ON tickets (tenant_id, kategorie); CREATE INDEX IF NOT EXISTS idx_timeline_ticket ON ticket_timeline (ticket_id); CREATE INDEX IF NOT EXISTS idx_audit_tenant_zeit ON audit_log (tenant_id, zeit DESC); CREATE INDEX IF NOT EXISTS idx_services_tenant ON services (tenant_id); CREATE INDEX IF NOT EXISTS idx_users_tenant ON users (tenant_id); -- Vorbereitung fuer spaetere AD/LDAP-Anbindung (Nutzer-Vorgabe 2026-07-13): -- 'local' = im ITSM selbst verwaltetes Konto (heutiger Stand), 'ldap' reserviert -- fuer die kommende Ausbaustufe. Kein Verhaltensunterschied heute, nur Vorbereitung. ALTER TABLE users ADD COLUMN IF NOT EXISTS auth_source TEXT NOT NULL DEFAULT 'local'; -- Zusaetzliche Nutzer-Details (Nutzer-Vorgabe 2026-07-13: "Mehr Details fuer -- die User (Telefon, Adresse, etc.)"). Alle optional, da nicht jeder Mandant -- diese Angaben pflegen moechte. ALTER TABLE users ADD COLUMN IF NOT EXISTS vorname TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS nachname TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS telefon TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS abteilung TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS adresse TEXT; -- ── Wissensdatenbank ────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS kb_articles ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, titel TEXT NOT NULL, kategorie TEXT NOT NULL DEFAULT 'Allgemein', inhalt TEXT NOT NULL, tags TEXT, autor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_kb_tenant_kategorie ON kb_articles (tenant_id, kategorie); -- ── CMDB (Configuration Management Database) ────────────────────────────────── CREATE TABLE IF NOT EXISTS configuration_items ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, ci_typ TEXT NOT NULL DEFAULT 'Sonstiges', status TEXT NOT NULL DEFAULT 'Aktiv', beschreibung TEXT, attribute JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_ci_tenant_typ ON configuration_items (tenant_id, ci_typ); CREATE INDEX IF NOT EXISTS idx_ci_tenant_status ON configuration_items (tenant_id, status); CREATE TABLE IF NOT EXISTS ci_relationships ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, from_ci_id INTEGER NOT NULL REFERENCES configuration_items(id) ON DELETE CASCADE, to_ci_id INTEGER NOT NULL REFERENCES configuration_items(id) ON DELETE CASCADE, beziehungs_typ TEXT NOT NULL DEFAULT 'haengt ab von', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_cirel_tenant ON ci_relationships (tenant_id); CREATE INDEX IF NOT EXISTS idx_cirel_from ON ci_relationships (from_ci_id); CREATE INDEX IF NOT EXISTS idx_cirel_to ON ci_relationships (to_ci_id); -- ═══════════════════════════════════════════════════════════════════════════════ -- Migration 2026-07-15: ITIL-v3/v4-Ausrichtung + Sicherheits-Haertung (Rust-Rewrite) -- Alle Aenderungen idempotent, damit init beim App-Start gefahrlos wiederholt laeuft. -- ═══════════════════════════════════════════════════════════════════════════════ -- ── Rollen (ITIL-orientiert) ────────────────────────────────────────────────── -- 'admin' (Service Owner), 'change_manager' (Change Enablement/CAB), -- 'agent' (Service Desk), 'user' (Requester/Self-Service). -- users.role existiert bereits als TEXT; erlaubte Werte prueft die App. -- ── Tickets: Impact/Urgency-Prioritaetsmatrix (ITIL v3, SO 4.2.5.4) ────────── ALTER TABLE tickets ADD COLUMN IF NOT EXISTS impact TEXT NOT NULL DEFAULT 'Mittel'; ALTER TABLE tickets ADD COLUMN IF NOT EXISTS urgency TEXT NOT NULL DEFAULT 'Mittel'; -- ── Tickets: Change Enablement (v4) / Change Management (v3) ────────────────── ALTER TABLE tickets ADD COLUMN IF NOT EXISTS change_typ TEXT; ALTER TABLE tickets ADD COLUMN IF NOT EXISTS approval_status TEXT; ALTER TABLE tickets ADD COLUMN IF NOT EXISTS approved_by INTEGER REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE tickets ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; -- ── Tickets: Problem Management (v3 SO 4.4 / v4 Practice) ───────────────────── ALTER TABLE tickets ADD COLUMN IF NOT EXISTS problem_id INTEGER REFERENCES tickets(id) ON DELETE SET NULL; ALTER TABLE tickets ADD COLUMN IF NOT EXISTS known_error BOOLEAN NOT NULL DEFAULT FALSE; CREATE INDEX IF NOT EXISTS idx_tickets_problem ON tickets (problem_id); -- ── Tickets: SLA-Zeitstempel (Service Level Management) ─────────────────────── ALTER TABLE tickets ADD COLUMN IF NOT EXISTS first_response_at TIMESTAMPTZ; ALTER TABLE tickets ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMPTZ; ALTER TABLE tickets ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ; -- ── Ticket-CI-Verknuepfung (SACM v3 / Service Configuration Management v4) ──── CREATE TABLE IF NOT EXISTS ticket_ci_links ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, ci_id INTEGER NOT NULL REFERENCES configuration_items(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (ticket_id, ci_id) ); CREATE INDEX IF NOT EXISTS idx_tcl_ticket ON ticket_ci_links (ticket_id); CREATE INDEX IF NOT EXISTS idx_tcl_ci ON ticket_ci_links (ci_id); -- ── Wissensdatenbank: Freigabe-Workflow (Knowledge Management) ──────────────── -- Bestehende Artikel werden einmalig 'Freigegeben'; neue starten als 'Entwurf'. ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS status TEXT; UPDATE kb_articles SET status='Freigegeben' WHERE status IS NULL; ALTER TABLE kb_articles ALTER COLUMN status SET DEFAULT 'Entwurf'; ALTER TABLE kb_articles ALTER COLUMN status SET NOT NULL; -- ── Login-Rate-Limiting (Brute-Force-Schutz, DB-basiert => multi-worker-fest) ── CREATE TABLE IF NOT EXISTS login_attempts ( id SERIAL PRIMARY KEY, email TEXT NOT NULL, ip TEXT, zeit TIMESTAMPTZ NOT NULL DEFAULT now(), success BOOLEAN NOT NULL DEFAULT FALSE ); CREATE INDEX IF NOT EXISTS idx_login_attempts_email_zeit ON login_attempts (email, zeit DESC); CREATE INDEX IF NOT EXISTS idx_login_attempts_ip_zeit ON login_attempts (ip, zeit DESC); -- ── Server-seitige Sessions (Rust-Rewrite 2026-07-15) ───────────────────────── -- In der DB liegt nur der SHA-256-Hash des Cookie-Tokens: ein DB-Leak -- kompromittiert keine laufenden Sessions; Sessions sind widerrufbar. CREATE TABLE IF NOT EXISTS sessions ( token_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, csrf_token TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions (user_id); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at);