-- Umsetzungsauftrag_Sonnet5.md Teil C, Phase 2, Schritt 12: Mandantenfaehigkeit -- vollstaendig aus dem Schema entfernen. -- -- Betreiberentscheidungen (Phase 0, per AskUserQuestion eingeholt): -- * tenant_admins: Benutzer wurden bereits VOR dieser Migration per -- scripts/promote_tenant_admins.py auf is_admin=1 -- befoerdert (Phase 1, mit eigenem Audit-Event je -- Benutzer) -- diese Migration setzt das nur noch -- schematisch um (Tabelle faellt weg). -- * users.home_tenant_id: VOLLSTAENDIGER Rebuild (nicht als tote Spalte -- belassen) -- daher ist 'users' hier mit dabei, -- zusaetzlich zu den fuenf primaer betroffenen -- Tabellen. -- * api_tokens: bestehende Tokens bleiben gueltig (KEIN -- Massenwiderruf) -- diese Migration entfernt nur -- die tenant_id-Spalte, ruehrt token_hash/ -- revoked_at/expires_at nicht an. -- -- ALTER TABLE ... DROP COLUMN scheidet fuer alle sechs Spalten aus (SQLite -- verweigert das Droppen indizierter/fremdschluesselbehafteter Spalten) -- -- deshalb voller Tabellen-Rebuild (12-Schritte-Verfahren) fuer host_groups, -- user_groups, ssh_keys, api_tokens, rdp_credentials und users. -- -- Reihenfolge wichtig: erst die fuenf idx_*_tenant-Indizes droppen, dann -- rebuilden, DANN tenant_admins/tenants droppen -- erst wenn keine -- REFERENCES tenants(id)-Klausel mehr im Schema steht, verschwindet die -- letzte Abhaengigkeit, die DROP TABLE tenants verhindern wuerde. -- -- Kein Tabellen-Rename der ALTEN Tabelle (z.B. "host_groups RENAME TO -- host_groups_old") -- das wuerde seit SQLite 3.25 (legacy_alter_table=OFF) -- automatisch alle REFERENCES host_groups(...)-Klauseln in ANDEREN Tabellen -- (z.B. hosts.host_group_id) auf den neuen Namen umschreiben, was hier -- explizit unerwuenscht ist. Stattdessen: neue Tabelle unter Platzhalter- -- namen anlegen, Daten kopieren, ALTE Tabelle direkt droppen (kein Rename), -- NEUE Tabelle auf den Zielnamen umbenennen -- der Platzhaltername taucht in -- keiner REFERENCES-Klausel auf, dieser Rename loest also keinen -- Seiteneffekt aus. -- -- Fremdschluesselgeprueft wird NICHT hier inline per PRAGMA foreign_key_check -- (das PRAGMA liefert nur eine Ergebnismenge zurueck, die ein per -- executescript() ausgefuehrtes Skript nicht auswertet) -- app/db.py:: -- _apply_migrations() fuehrt diese Pruefung nach JEDER Migration -- programmatisch aus und bricht den Start mit RuntimeError ab, falls -- Verletzungen gefunden werden. -- -- Audit-Log: historische tenant_created/tenant_updated/tenant_deleted/ -- tenant_admin_granted/tenant_admin_revoked/tenant_admin_promoted_to_admin- -- Eintraege werden NICHT angefasst (audit_log hat ohnehin keine -- Tenant-Spalte) -- die Trigger no_audit_update/no_audit_delete -- (0001_initial.sql) wuerden das ohnehin verweigern. Sie bleiben dauerhaft -- im (manipulationssicheren) Log erhalten. -- -- Verifiziert (Testharness gegen synthetischen Zwei-Mandanten-Datensatz, -- ausserhalb dieses Repos, da hier keine echte Produktiv-DB verfuegbar -- ist): Zeilenzahlen, Datenwerte, foreign_key_check, integrity_check, -- Audit-Hash-Kette und die Phase-1-Beforderung ueberstehen die Migration -- unveraendert. PRAGMA foreign_keys = OFF; DROP INDEX IF EXISTS idx_host_groups_tenant; DROP INDEX IF EXISTS idx_user_groups_tenant; DROP INDEX IF EXISTS idx_ssh_keys_tenant; DROP INDEX IF EXISTS idx_api_tokens_tenant; DROP INDEX IF EXISTS idx_rdp_credentials_tenant; DROP INDEX IF EXISTS idx_users_home_tenant; BEGIN IMMEDIATE; -- --- host_groups ------------------------------------------------------------ CREATE TABLE host_groups_new ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, description TEXT ); INSERT INTO host_groups_new (id, name, description) SELECT id, name, description FROM host_groups; DROP TABLE host_groups; ALTER TABLE host_groups_new RENAME TO host_groups; -- --- user_groups -------------------------------------------------------- CREATE TABLE user_groups_new ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, description TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); INSERT INTO user_groups_new (id, name, description, created_at) SELECT id, name, description, created_at FROM user_groups; DROP TABLE user_groups; ALTER TABLE user_groups_new RENAME TO user_groups; -- --- ssh_keys ------------------------------------------------------------- CREATE TABLE ssh_keys_new ( 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, passphrase_enc BLOB, username TEXT ); INSERT INTO ssh_keys_new ( id, label, owner_user_id, private_key_enc, public_key, key_type, created_at, rotated_at, expires_at, passphrase_enc, username ) SELECT id, label, owner_user_id, private_key_enc, public_key, key_type, created_at, rotated_at, expires_at, passphrase_enc, username FROM ssh_keys; DROP TABLE ssh_keys; ALTER TABLE ssh_keys_new RENAME TO ssh_keys; -- --- api_tokens ----------------------------------------------------------- CREATE TABLE api_tokens_new ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, label TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE, token_prefix TEXT NOT NULL, scopes_json TEXT NOT NULL, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), expires_at TEXT, last_used_at TEXT, revoked_at TEXT ); INSERT INTO api_tokens_new ( id, user_id, label, token_hash, token_prefix, scopes_json, created_by, created_at, expires_at, last_used_at, revoked_at ) SELECT id, user_id, label, token_hash, token_prefix, scopes_json, created_by, created_at, expires_at, last_used_at, revoked_at FROM api_tokens; DROP TABLE api_tokens; ALTER TABLE api_tokens_new RENAME TO api_tokens; CREATE INDEX IF NOT EXISTS idx_api_tokens_user ON api_tokens(user_id); -- --- rdp_credentials -------------------------------------------------------- CREATE TABLE rdp_credentials_new ( id INTEGER PRIMARY KEY, label TEXT NOT NULL, username TEXT NOT NULL, domain TEXT, password_enc BLOB NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), rotated_at TEXT ); INSERT INTO rdp_credentials_new (id, label, username, domain, password_enc, created_at, rotated_at) SELECT id, label, username, domain, password_enc, created_at, rotated_at FROM rdp_credentials; DROP TABLE rdp_credentials; ALTER TABLE rdp_credentials_new RENAME TO rdp_credentials; -- --- users (Betreiberentscheidung: voller Rebuild statt tote Spalte) ----- CREATE TABLE users_new ( 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, session_version INTEGER NOT NULL DEFAULT 1, deleted_at TEXT ); INSERT INTO users_new ( id, username, password_hash, totp_secret_enc, totp_enrolled, is_active, is_admin, failed_logins, locked_until, created_at, password_changed_at, must_change_password, session_version, deleted_at ) SELECT id, username, password_hash, totp_secret_enc, totp_enrolled, is_active, is_admin, failed_logins, locked_until, created_at, password_changed_at, must_change_password, session_version, deleted_at FROM users; DROP TABLE users; ALTER TABLE users_new RENAME TO users; -- --- tenant_admins/tenants zuletzt -- erst jetzt existiert keine -- REFERENCES tenants(id)-Klausel mehr im Schema (alle sechs Tabellen oben -- wurden bereits ohne tenant_id/home_tenant_id neu angelegt). DROP INDEX IF EXISTS idx_tenant_admins_user; DROP TABLE tenant_admins; DROP TABLE tenants; COMMIT; PRAGMA foreign_keys = ON;