Files
ssh-jumphost/app/db/migrations/0006_tenants.sql
2026-08-20 15:47:50 +02:00

92 lines
5.4 KiB
SQL

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