Files
ssh-jumphost/app/db/migrations/0014_drop_tenants.sql
2026-09-02 20:30:44 +02:00

197 lines
8.7 KiB
SQL

-- 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;