-- Mandantenfaehigkeit (volle Isolation, siehe Konzept-Erweiterung dieser -- Session): jede Hostgruppe, Benutzergruppe, jeder SSH-Key und jedes -- API-Token gehoert genau einem Mandanten. Ein Benutzer kann Mandanten-Admin -- fuer einen oder mehrere Mandanten sein (tenant_admins) -- er sieht/ -- verwaltet dann AUSSCHLIESSLICH Ressourcen seines/seiner Mandanten -- (Durchsetzung zentral in app/tenancy.py + app/auth/deps.py, angewendet in -- jedem Endpunkt in app/admin/routes.py). Das bestehende users.is_admin -- bleibt unveraendert bestehen und wird zum "Super-Admin": sieht/verwaltet -- weiterhin ALLE Mandanten uebergreifend (volle Abwaertskompatibilitaet zu -- allen bisherigen Admin-Workflows dieser App). -- -- Rueckwirkende Kompatibilitaet: ein Standard-Mandant (id=1) wird automatisch -- angelegt und alle VOR dieser Migration bestehenden Hostgruppen/ -- Benutzergruppen/SSH-Keys/API-Tokens werden ihm zugeordnet -- fuer -- Super-Admins aendert sich dadurch am sichtbaren Verhalten nichts. CREATE TABLE IF NOT EXISTS tenants ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, description TEXT, is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); INSERT OR IGNORE INTO tenants (id, name, description) VALUES (1, 'Standard', 'Automatisch angelegter Standard-Mandant (Daten von vor der Einfuehrung der Mandantenfaehigkeit).'); -- Mandanten-Admin-Zuweisung ist bewusst eine eigene Tabelle (statt eines -- einzelnen users.tenant_id) -- ein User kann Mandanten-Admin fuer MEHRERE -- Mandanten sein (z.B. externer Dienstleister fuer mehrere Kunden), ohne -- gleich globaler Super-Admin zu sein. Nur ein Super-Admin darf diese -- Zuweisung vornehmen (require_global_admin, siehe admin/routes.py). CREATE TABLE IF NOT EXISTS tenant_admins ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, granted_by INTEGER REFERENCES users(id), granted_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), PRIMARY KEY (user_id, tenant_id) ); CREATE INDEX IF NOT EXISTS idx_tenant_admins_user ON tenant_admins(user_id); -- WICHTIG zu ALTER TABLE ADD COLUMN + REFERENCES in SQLite: eine per ALTER -- TABLE nachtraeglich hinzugefuegte Spalte mit REFERENCES-Klausel darf NUR -- einen NULL-Default haben ("Cannot add a REFERENCES column with non-NULL -- default value") -- UND eine NOT NULL-Klausel wiederum erfordert einen -- Default ungleich NULL. Beides gleichzeitig (NOT NULL DEFAULT 1 REFERENCES -- ...) ist beim nachtraeglichen Hinzufuegen einer Spalte also grundsaetzlich -- unmoeglich (nur bei CREATE TABLE erlaubt) -- ein vollstaendiger -- Tabellen-Rebuild (SQLite-12-Schritte-Verfahren) waere die einzige -- Alternative, wird hier aber bewusst vermieden, um bestehende -- Produktivdaten nicht bei jedem Deployment einem Rebuild-Risiko -- auszusetzen. Stattdessen: Spalte NULLABLE mit REFERENCES hinzufuegen, -- bestehende Zeilen sofort auf den Standard-Mandanten (id=1) zurueckfuellen. -- Die Nicht-NULL-Garantie wird stattdessen ausschliesslich anwendungsseitig -- durchgesetzt: JEDER Insert-Pfad in app/admin/routes.py ermittelt -- tenant_id ueber _resolve_write_tenant() (nie NULL, siehe app/tenancy.py) -- -- bestehende Zeilen sind durch das Backfill unten ebenfalls nie NULL. ALTER TABLE host_groups ADD COLUMN tenant_id INTEGER REFERENCES tenants(id); UPDATE host_groups SET tenant_id = 1 WHERE tenant_id IS NULL; ALTER TABLE user_groups ADD COLUMN tenant_id INTEGER REFERENCES tenants(id); UPDATE user_groups SET tenant_id = 1 WHERE tenant_id IS NULL; ALTER TABLE ssh_keys ADD COLUMN tenant_id INTEGER REFERENCES tenants(id); UPDATE ssh_keys SET tenant_id = 1 WHERE tenant_id IS NULL; -- api_tokens: konzeptionell bewusst NICHT nullable -- jedes Token (auch von -- einem Super-Admin erzeugte) ist genau einem Mandanten zugeordnet. Ein -- Token ist (wie schon vor dieser Migration) nie is_admin=True; ohne -- verpflichtenden Mandantenbezug waere unklar, welche Mandanten-Ressourcen -- es sehen darf -- lieber explizit pro Mandant ein Token ausstellen als eine -- mehrdeutige "gilt ueberall"-Sonderregel einzufuehren (Prinzip: fail -- closed). Schema-technisch aus obigem Grund dennoch nullable + Backfill; -- app/admin/routes.py::create_api_token() setzt tenant_id auf jedem -- Insert-Pfad immer explizit. ALTER TABLE api_tokens ADD COLUMN tenant_id INTEGER REFERENCES tenants(id); UPDATE api_tokens SET tenant_id = 1 WHERE tenant_id IS NULL; -- users.home_tenant_id: rein informativ/UX -- der Mandant, "fuer" den ein -- Benutzer angelegt wurde (automatisch gesetzt, wenn ein Mandanten-Admin den -- User anlegt), damit ein frisch angelegter User sofort in der Mandanten- -- Admin-Sicht auftaucht, auch bevor er einer Gruppe zugeordnet oder ihm eine -- Rolle gewaehrt wurde. Erzwingt KEINE Zugriffsbeschraenkung selbst -- die -- eigentliche Sichtbarkeit/Isolation ergibt sich weiterhin aus Rollen- -- Vergabe auf Hostgruppen eines Mandanten (siehe app/tenancy.py). ALTER TABLE users ADD COLUMN home_tenant_id INTEGER REFERENCES tenants(id); CREATE INDEX IF NOT EXISTS idx_host_groups_tenant ON host_groups(tenant_id); CREATE INDEX IF NOT EXISTS idx_user_groups_tenant ON user_groups(tenant_id); CREATE INDEX IF NOT EXISTS idx_ssh_keys_tenant ON ssh_keys(tenant_id); CREATE INDEX IF NOT EXISTS idx_api_tokens_tenant ON api_tokens(tenant_id); CREATE INDEX IF NOT EXISTS idx_users_home_tenant ON users(home_tenant_id);