-- Initiales Schema. Siehe Konzeptdokument Kap. 5. -- Wird vom Migration-Runner (app/db.py) einmalig und idempotent angewendet. PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, totp_secret_enc BLOB, totp_enrolled INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, is_admin INTEGER NOT NULL DEFAULT 0, failed_logins INTEGER NOT NULL DEFAULT 0, locked_until TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), password_changed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), must_change_password INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS recovery_codes ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, code_hash TEXT NOT NULL, used_at TEXT ); CREATE INDEX IF NOT EXISTS idx_recovery_codes_user ON recovery_codes(user_id); CREATE TABLE IF NOT EXISTS roles ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL ); INSERT OR IGNORE INTO roles (id, name) VALUES (1, 'ssh_connect'), (2, 'rdp_connect'), (3, 'file_transfer'), (4, 'clipboard'), (5, 'session_recording_view'), (6, 'admin_hostgroup'); CREATE TABLE IF NOT EXISTS host_groups ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, description TEXT ); CREATE TABLE IF NOT EXISTS hosts ( id INTEGER PRIMARY KEY, host_group_id INTEGER NOT NULL REFERENCES host_groups(id), hostname TEXT NOT NULL, address TEXT NOT NULL, protocol TEXT NOT NULL CHECK (protocol IN ('ssh','rdp')), port INTEGER NOT NULL, os_type TEXT NOT NULL CHECK (os_type IN ('linux','windows')), ssh_host_key_fingerprint TEXT, ssh_username TEXT, rdp_username TEXT, rdp_domain TEXT, rdp_require_nla INTEGER NOT NULL DEFAULT 1, clipboard_enabled INTEGER NOT NULL DEFAULT 1, file_transfer_enabled INTEGER NOT NULL DEFAULT 1, is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); CREATE INDEX IF NOT EXISTS idx_hosts_group ON hosts(host_group_id); CREATE TABLE IF NOT EXISTS ssh_keys ( id INTEGER PRIMARY KEY, label TEXT NOT NULL, owner_user_id INTEGER REFERENCES users(id), private_key_enc BLOB NOT NULL, public_key TEXT NOT NULL, key_type TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), rotated_at TEXT, expires_at TEXT ); CREATE TABLE IF NOT EXISTS host_ssh_key_map ( host_id INTEGER NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, ssh_key_id INTEGER NOT NULL REFERENCES ssh_keys(id), PRIMARY KEY (host_id, ssh_key_id) ); CREATE TABLE IF NOT EXISTS user_hostgroup_roles ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, host_group_id INTEGER NOT NULL REFERENCES host_groups(id) ON DELETE CASCADE, role_id INTEGER NOT NULL REFERENCES roles(id), granted_by INTEGER REFERENCES users(id), granted_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), expires_at TEXT, PRIMARY KEY (user_id, host_group_id, role_id) ); CREATE INDEX IF NOT EXISTS idx_uhr_user ON user_hostgroup_roles(user_id); CREATE TABLE IF NOT EXISTS sessions ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), host_id INTEGER NOT NULL REFERENCES hosts(id), protocol TEXT NOT NULL, started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), ended_at TEXT, client_ip TEXT NOT NULL, recording_path TEXT, end_reason TEXT ); CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_host ON sessions(host_id); CREATE TABLE IF NOT EXISTS file_transfers ( id INTEGER PRIMARY KEY, session_id INTEGER NOT NULL REFERENCES sessions(id), direction TEXT NOT NULL CHECK (direction IN ('upload','download')), filename TEXT NOT NULL, size_bytes INTEGER NOT NULL, sha256 TEXT NOT NULL, av_scan_result TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); CREATE INDEX IF NOT EXISTS idx_ft_session ON file_transfers(session_id); CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY, ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), user_id INTEGER REFERENCES users(id), client_ip TEXT, event_type TEXT NOT NULL, details_json TEXT NOT NULL, prev_hash TEXT NOT NULL, entry_hash TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(ts); CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id); -- Audit-Log ist auf DB-Ebene append-only: UPDATE/DELETE werden hart verweigert. CREATE TRIGGER IF NOT EXISTS no_audit_update BEFORE UPDATE ON audit_log BEGIN SELECT RAISE(ABORT, 'audit_log ist append-only'); END; CREATE TRIGGER IF NOT EXISTS no_audit_delete BEFORE DELETE ON audit_log BEGIN SELECT RAISE(ABORT, 'audit_log ist append-only'); END; CREATE TABLE IF NOT EXISTS schema_migrations ( filename TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) );