umbau 1.0
This commit is contained in:
21
app/db/migrations/0013_fix_placeholder_rdp_username.sql
Normal file
21
app/db/migrations/0013_fix_placeholder_rdp_username.sql
Normal file
@ -0,0 +1,21 @@
|
||||
-- Migration 0013: Befund C2 (Umsetzungsauftrag_Sonnet5.md Teil A.3).
|
||||
--
|
||||
-- Migration 0012 hat fuer RDP-Zugangsdatensaetze ohne Benutzername am alten
|
||||
-- Host-Datensatz den Platzhaltertext '(kein Benutzername)' eingetragen,
|
||||
-- statt die Spalte leer zu lassen. Die Leerpruefung in
|
||||
-- app/rdp_proxy/guacd_client.py::build_rdp_params() greift nur bei einem
|
||||
-- LEEREN String -- der Platzhalter ist nicht leer und wurde bislang als
|
||||
-- echter RDP-Anmeldename an Windows gesendet. Das Ziel meldet daraufhin
|
||||
-- "Anmeldung fehlgeschlagen" (bzw. guacd Status 769), was wie ein falsches
|
||||
-- Passwort aussieht statt wie das eigentliche Problem: es fehlt schlicht
|
||||
-- ein Benutzername.
|
||||
--
|
||||
-- Ruecksetzung auf einen leeren String macht die vorhandene, bereits
|
||||
-- korrekte Leerpruefung wieder wirksam -- inkl. ihres Fallbacks auf
|
||||
-- hosts.rdp_username (Altbestand) und ihrer sprechenden Fehlermeldung,
|
||||
-- wenn auch das leer ist. Betroffen sind ausschliesslich Datensaetze mit
|
||||
-- exakt diesem Migrations-Platzhalter; von Hand vergebene, echte
|
||||
-- Benutzernamen bleiben unberuehrt.
|
||||
UPDATE rdp_credentials
|
||||
SET username = ''
|
||||
WHERE username = '(kein Benutzername)';
|
||||
196
app/db/migrations/0014_drop_tenants.sql
Normal file
196
app/db/migrations/0014_drop_tenants.sql
Normal file
@ -0,0 +1,196 @@
|
||||
-- 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;
|
||||
34
app/db/migrations/0015_remove_dead_roles.sql
Normal file
34
app/db/migrations/0015_remove_dead_roles.sql
Normal file
@ -0,0 +1,34 @@
|
||||
-- Teil D.6 Schritt 1 (Umsetzungsauftrag_Sonnet5.md): drei nie durchgesetzte
|
||||
-- ("tote") Rollen entfernen, je nach Betreiberentscheidung. Der Betreiber
|
||||
-- hat sich fuer alle drei fuer "Entfernen" statt "Ausimplementieren"
|
||||
-- entschieden (Rueckfrage vom 2026-08-31):
|
||||
--
|
||||
-- * clipboard -- Clipboard bleibt ausschliesslich ueber das
|
||||
-- host-globale hosts.clipboard_enabled
|
||||
-- gesteuert (heutiges Verhalten, unveraendert).
|
||||
-- * session_recording_view -- Sitzungs-Playback bleibt dauerhaft
|
||||
-- ausschliesslich globalen Admins vorbehalten
|
||||
-- (require_global_admin, unveraendert).
|
||||
-- * admin_hostgroup -- keine delegierte Hostgruppen-Admin-Ebene;
|
||||
-- es bleibt bei genau zwei Stufen (globaler
|
||||
-- Admin vs. granulare Rollen ueber Gruppen).
|
||||
--
|
||||
-- Diese drei Rollen waren seit 0001_initial.sql vergebbar, aber kein
|
||||
-- einziger Codepfad hat sie je geprueft (siehe D.1 im Umsetzungsauftrag) --
|
||||
-- das Entfernen aendert daher am tatsaechlichen Verhalten NICHTS, schliesst
|
||||
-- aber die falsche Sicherheitserwartung, dass eine Vergabe dieser Rollen
|
||||
-- irgendeine Wirkung haette.
|
||||
--
|
||||
-- Reihenfolge wichtig: zuerst alle Vergaben dieser Rollen loeschen (FK
|
||||
-- role_id -> roles(id) hat keine eigene ON DELETE-Klausel, siehe
|
||||
-- 0001_initial.sql:88/0004_user_groups.sql:28), erst danach die
|
||||
-- roles-Zeilen selbst.
|
||||
|
||||
DELETE FROM user_hostgroup_roles
|
||||
WHERE role_id IN (SELECT id FROM roles WHERE name IN ('clipboard', 'session_recording_view', 'admin_hostgroup'));
|
||||
|
||||
DELETE FROM group_hostgroup_roles
|
||||
WHERE role_id IN (SELECT id FROM roles WHERE name IN ('clipboard', 'session_recording_view', 'admin_hostgroup'));
|
||||
|
||||
DELETE FROM roles
|
||||
WHERE name IN ('clipboard', 'session_recording_view', 'admin_hostgroup');
|
||||
49
app/db/migrations/0016_ssh_password_credential_objects.sql
Normal file
49
app/db/migrations/0016_ssh_password_credential_objects.sql
Normal file
@ -0,0 +1,49 @@
|
||||
-- Migration 0016: SSH-Passwort-Zugangsdaten als eigenstaendiges Objekt mit
|
||||
-- eigener ID -- Vorarbeit fuer Teil D.6 Schritt 3 (Umsetzungsauftrag_Sonnet5.md
|
||||
-- D.3): Achse B ("Benutzergruppe x Zugangsdatensatz") braucht fuer alle drei
|
||||
-- Credential-Arten (SSH-Key, RDP, SSH-Passwort) dieselbe Form -- eine
|
||||
-- Freigabetabelle referenziert eine ID, keine host_id. SSH-Keys und (seit
|
||||
-- Migration 0012) RDP-Zugangsdaten haben das bereits; SSH-Passwoerter noch
|
||||
-- nicht (0011_ssh_password_credentials.sql: host_id war PRIMARY KEY, ein
|
||||
-- Datensatz ausschliesslich 1:1 an genau einem Host).
|
||||
--
|
||||
-- Vorgehen exakt analog zu Migration 0012 (siehe dortiger Kommentar fuer die
|
||||
-- ausfuehrliche Begruendung): alte 1:1-Tabelle bleibt als Datenquelle
|
||||
-- erhalten, wird umbenannt; neues eigenstaendiges Objekt plus 1:1-
|
||||
-- Zuordnungstabelle (bewusst weiterhin PRIMARY KEY auf host_id -- diese
|
||||
-- Migration fuehrt NICHT die Mehrfachzuweisung-an-mehrere-Hosts-UI ein, die
|
||||
-- RDP inzwischen hat; das waere ein eigener, spaeterer Schritt. Hier geht es
|
||||
-- ausschliesslich darum, dass das Objekt eine stabile ID hat, auf die eine
|
||||
-- Achse-B-Freigabetabelle verweisen kann).
|
||||
--
|
||||
-- Korrelation zwischen den beiden folgenden INSERTs laeuft wie bei 0012
|
||||
-- ueber password_enc statt ueber eine temporaere ID-Spalte: jede
|
||||
-- Verschluesselung verwendet einen frischen Zufalls-Nonce (app/security/
|
||||
-- crypto.py::encrypt_secret), zwei Zeilen der Alttabelle koennen also nie
|
||||
-- denselben password_enc-Wert haben.
|
||||
|
||||
ALTER TABLE ssh_password_credentials RENAME TO ssh_password_credentials_legacy;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_password_credentials (
|
||||
id INTEGER PRIMARY KEY,
|
||||
label TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
password_enc BLOB NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
||||
rotated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS host_ssh_password_credential_map (
|
||||
host_id INTEGER PRIMARY KEY REFERENCES hosts(id) ON DELETE CASCADE,
|
||||
ssh_password_credential_id INTEGER NOT NULL REFERENCES ssh_password_credentials(id)
|
||||
);
|
||||
|
||||
INSERT INTO ssh_password_credentials (label, username, password_enc, created_at)
|
||||
SELECT 'Migriert: ' || h.hostname, spcl.username, spcl.password_enc, spcl.updated_at
|
||||
FROM ssh_password_credentials_legacy spcl
|
||||
JOIN hosts h ON h.id = spcl.host_id;
|
||||
|
||||
INSERT INTO host_ssh_password_credential_map (host_id, ssh_password_credential_id)
|
||||
SELECT spcl.host_id, spc.id
|
||||
FROM ssh_password_credentials_legacy spcl
|
||||
JOIN ssh_password_credentials spc ON spc.password_enc = spcl.password_enc;
|
||||
64
app/db/migrations/0017_personal_groups.sql
Normal file
64
app/db/migrations/0017_personal_groups.sql
Normal file
@ -0,0 +1,64 @@
|
||||
-- Migration 0017: persoenliche Gruppen als rechteerhaltende Migration weg
|
||||
-- von Direktvergaben (user_hostgroup_roles) -- Teil D.6 Schritt 3, D.4
|
||||
-- (Umsetzungsauftrag_Sonnet5.md). Empfohlener Ansatz dort: "automatisch
|
||||
-- erzeugte persoenliche Gruppe je betroffenem Benutzer, danach manuelle
|
||||
-- Konsolidierung" -- niemand verliert oder gewinnt beim Umstieg etwas.
|
||||
--
|
||||
-- user_hostgroup_roles bleibt nach dieser Migration bestehen (Projekt-
|
||||
-- konvention: Altes bleibt lesbar erhalten statt geloescht, siehe 0010/0012)
|
||||
-- und wird auch weiterhin von app/rbac.py ausgewertet -- das Leeren/
|
||||
-- Umbenennen dieser Tabelle ist D.6 Schritt 5 (Schreibpfade abschalten),
|
||||
-- NICHT dieser Schritt. Diese Migration fuegt nur die Gruppen-Spiegelung
|
||||
-- HINZU, sie nimmt niemandem etwas weg.
|
||||
|
||||
-- D.7-Risiko "Rechteausweitung ueber persoenliche Gruppen": Kennzeichen
|
||||
-- "persoenlich" (fuer UI/Reports in Schritt 6) plus Trigger-Sperre "max. 1
|
||||
-- Mitglied" weiter unten. ADD COLUMN mit konstantem Default ist in SQLite
|
||||
-- ohne Tabellen-Rebuild moeglich.
|
||||
ALTER TABLE user_groups ADD COLUMN is_personal INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS enforce_personal_group_single_member
|
||||
BEFORE INSERT ON user_group_members
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT is_personal FROM user_groups WHERE id = NEW.user_group_id) = 1
|
||||
AND (SELECT COUNT(*) FROM user_group_members WHERE user_group_id = NEW.user_group_id) >= 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'Persoenliche Gruppe darf nur ein Mitglied haben');
|
||||
END;
|
||||
|
||||
-- Eine persoenliche Gruppe je Benutzer mit mindestens einer Direktvergabe.
|
||||
-- Name enthaelt bewusst die User-ID (nicht nur den Benutzernamen), um jede
|
||||
-- Kollision mit einer bereits existierenden, gleichnamigen Gruppe
|
||||
-- auszuschliessen (user_groups.name ist UNIQUE NOT NULL) -- ein Fehlschlag
|
||||
-- hier wuerde die gesamte Migration abbrechen. Explizite Transaktion fuer
|
||||
-- Atomaritaet (alle drei INSERTs oder keiner) -- anders als 0014 wird hier
|
||||
-- nichts umgebaut/gedroppt, PRAGMA foreign_keys = OFF ist daher nicht noetig
|
||||
-- (keine der drei INSERT-Anweisungen kann eine gueltige Fremdschluessel-
|
||||
-- Referenz verletzen).
|
||||
BEGIN IMMEDIATE;
|
||||
|
||||
INSERT INTO user_groups (name, description, is_personal)
|
||||
SELECT 'Persoenlich: ' || u.username || ' (#' || u.id || ')',
|
||||
'Automatisch erzeugt bei der Migration auf ausschliesslich '
|
||||
|| 'gruppenbasierte Berechtigungen (Teil D.6 Schritt 3, Migration '
|
||||
|| '0017) -- Spiegel der vormaligen Direktvergaben dieses Benutzers '
|
||||
|| 'aus user_hostgroup_roles. Kann spaeter bewusst in eine echte '
|
||||
|| 'Team-Gruppe ueberfuehrt werden (D.6 Schritt 6).',
|
||||
1
|
||||
FROM users u
|
||||
WHERE u.id IN (SELECT DISTINCT user_id FROM user_hostgroup_roles)
|
||||
AND u.deleted_at IS NULL;
|
||||
|
||||
INSERT INTO user_group_members (user_group_id, user_id, added_by, added_at)
|
||||
SELECT ug.id, u.id, NULL, strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
FROM user_groups ug
|
||||
JOIN users u ON ug.name = 'Persoenlich: ' || u.username || ' (#' || u.id || ')'
|
||||
WHERE ug.is_personal = 1;
|
||||
|
||||
INSERT INTO group_hostgroup_roles (user_group_id, host_group_id, role_id, granted_by, granted_at, expires_at)
|
||||
SELECT ug.id, uhr.host_group_id, uhr.role_id, uhr.granted_by, uhr.granted_at, uhr.expires_at
|
||||
FROM user_hostgroup_roles uhr
|
||||
JOIN users u ON u.id = uhr.user_id
|
||||
JOIN user_groups ug ON ug.name = 'Persoenlich: ' || u.username || ' (#' || u.id || ')' AND ug.is_personal = 1;
|
||||
|
||||
COMMIT;
|
||||
91
app/db/migrations/0018_credential_group_grants.sql
Normal file
91
app/db/migrations/0018_credential_group_grants.sql
Normal file
@ -0,0 +1,91 @@
|
||||
-- Migration 0018: Achse B (Umsetzungsauftrag_Sonnet5.md Teil D.3) --
|
||||
-- "Benutzergruppe x Zugangsdatensatz". Beantwortet erstmals, WOMIT sich eine
|
||||
-- Gruppe anmeldet: eine Verbindung soll kuenftig nur zustande kommen, wenn
|
||||
-- ein Zugangsdatensatz existiert, der SOWOHL dem Host zugeordnet ALS AUCH
|
||||
-- einer Gruppe des Benutzers freigegeben ist (Achse A -- ssh_connect/
|
||||
-- rdp_connect je Hostgruppe -- bleibt unveraendert und bestimmt weiterhin,
|
||||
-- OB ueberhaupt verbunden werden darf).
|
||||
--
|
||||
-- Drei schmale Freigabetabellen, eine je Credential-Art, Aufbau analog zu
|
||||
-- group_hostgroup_roles (zusammengesetzter PK, FK auf user_groups mit ON
|
||||
-- DELETE CASCADE, FK auf das jeweilige Credential-Objekt, granted_by/
|
||||
-- granted_at/expires_at). Alle drei Credential-Arten haben seit Migration
|
||||
-- 0012 (RDP) bzw. 0016 (SSH-Passwort, diese Session) eine eigene stabile ID
|
||||
-- -- SSH-Keys hatten sie schon immer (0001_initial.sql).
|
||||
--
|
||||
-- WICHTIG: diese Migration schafft nur das Schema UND befuellt es
|
||||
-- rechteneutral (siehe unten) -- app/rbac.py/die Proxies pruefen diese
|
||||
-- Tabellen noch NICHT (das ist D.6 Schritt 4, "Lesepfade umstellen", noch
|
||||
-- nicht umgesetzt). Bis dahin bleibt das heutige Verhalten (jeder Nutzer mit
|
||||
-- ssh_connect/rdp_connect auf der Hostgruppe nutzt automatisch das am Host
|
||||
-- haengende Credential) unveraendert in Kraft -- diese Migration allein
|
||||
-- aendert also NICHTS am Laufzeitverhalten.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_ssh_key_grants (
|
||||
user_group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||
ssh_key_id INTEGER NOT NULL REFERENCES ssh_keys(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_group_id, ssh_key_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_gskg_group ON group_ssh_key_grants(user_group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_gskg_key ON group_ssh_key_grants(ssh_key_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_rdp_credential_grants (
|
||||
user_group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||
rdp_credential_id INTEGER NOT NULL REFERENCES rdp_credentials(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_group_id, rdp_credential_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_grcg_group ON group_rdp_credential_grants(user_group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_grcg_cred ON group_rdp_credential_grants(rdp_credential_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_ssh_password_credential_grants (
|
||||
user_group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||
ssh_password_credential_id INTEGER NOT NULL REFERENCES ssh_password_credentials(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_group_id, ssh_password_credential_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_gspcg_group ON group_ssh_password_credential_grants(user_group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_gspcg_cred ON group_ssh_password_credential_grants(ssh_password_credential_id);
|
||||
|
||||
-- Rechteneutrale Vorbefuellung (D.4 Schritt 5): fuer jeden Host mit
|
||||
-- zugeordnetem Zugangsdatensatz wird jede Gruppe freigeschaltet, die auf der
|
||||
-- Hostgruppe dieses Hosts die passende Connect-Rolle haelt -- reproduziert
|
||||
-- exakt das heutige (Achse-A-only) Verhalten, bei dem JEDER Nutzer mit
|
||||
-- ssh_connect/rdp_connect automatisch das am Host haengende Credential
|
||||
-- benutzen darf. INSERT OR IGNORE, weil dieselbe (Gruppe, Credential)-
|
||||
-- Kombination ueber mehrere Hosts hinweg mehrfach auftreten kann (ein
|
||||
-- SSH-Key kann an mehreren Hosts derselben Hostgruppe haengen).
|
||||
--
|
||||
-- Setzt group_hostgroup_roles NACH der Personengruppen-Spiegelung aus
|
||||
-- Migration 0017 voraus (Reihenfolge 0017 vor 0018 ist deshalb zwingend) --
|
||||
-- sonst wuerden Nutzer, die ihre Verbindungsrechte bisher nur direkt (nicht
|
||||
-- ueber eine Gruppe) hatten, hier uebergangen.
|
||||
|
||||
INSERT OR IGNORE INTO group_ssh_key_grants (user_group_id, ssh_key_id, granted_by, granted_at)
|
||||
SELECT DISTINCT ghr.user_group_id, hskm.ssh_key_id, NULL, strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
FROM host_ssh_key_map hskm
|
||||
JOIN hosts h ON h.id = hskm.host_id
|
||||
JOIN group_hostgroup_roles ghr ON ghr.host_group_id = h.host_group_id
|
||||
JOIN roles r ON r.id = ghr.role_id AND r.name = 'ssh_connect';
|
||||
|
||||
INSERT OR IGNORE INTO group_rdp_credential_grants (user_group_id, rdp_credential_id, granted_by, granted_at)
|
||||
SELECT DISTINCT ghr.user_group_id, hrcm.rdp_credential_id, NULL, strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
FROM host_rdp_credential_map hrcm
|
||||
JOIN hosts h ON h.id = hrcm.host_id
|
||||
JOIN group_hostgroup_roles ghr ON ghr.host_group_id = h.host_group_id
|
||||
JOIN roles r ON r.id = ghr.role_id AND r.name = 'rdp_connect';
|
||||
|
||||
INSERT OR IGNORE INTO group_ssh_password_credential_grants
|
||||
(user_group_id, ssh_password_credential_id, granted_by, granted_at)
|
||||
SELECT DISTINCT ghr.user_group_id, hspcm.ssh_password_credential_id, NULL, strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
FROM host_ssh_password_credential_map hspcm
|
||||
JOIN hosts h ON h.id = hspcm.host_id
|
||||
JOIN group_hostgroup_roles ghr ON ghr.host_group_id = h.host_group_id
|
||||
JOIN roles r ON r.id = ghr.role_id AND r.name = 'ssh_connect';
|
||||
26
app/db/migrations/0019_retire_direct_role_grants.sql
Normal file
26
app/db/migrations/0019_retire_direct_role_grants.sql
Normal file
@ -0,0 +1,26 @@
|
||||
-- Migration 0019: Direktvergabe von Hostgruppen-Rollen retiriert (Teil D.6
|
||||
-- Schritt 5, Umsetzungsauftrag_Sonnet5.md D.4 Punkt 4: "user_hostgroup_roles
|
||||
-- leeren bzw. umbenennen").
|
||||
--
|
||||
-- Ab diesem Codestand ist `user_hostgroup_roles` von KEINEM Lese- oder
|
||||
-- Schreibpfad der Anwendung mehr aus erreichbar:
|
||||
-- * Lesepfade: app/rbac.py liest die Tabelle bereits seit Migration 0017 +
|
||||
-- Schritt 4 (Codeaenderung, keine eigene Migration) nicht mehr -- jede
|
||||
-- vormals direkte Vergabe wurde durch Migration 0017 1:1 in eine
|
||||
-- persoenliche Gruppe gespiegelt (group_hostgroup_roles).
|
||||
-- * Schreibpfade: POST /admin/roles/grant und /admin/roles/revoke
|
||||
-- (app/admin/routes.py) antworten seit derselben Codeaenderung mit HTTP
|
||||
-- 410 statt in die Tabelle zu schreiben.
|
||||
--
|
||||
-- Projektkonvention (siehe 0010:9-14, 0012:12-17, 0016 oben): eine Tabelle
|
||||
-- wird beim Rueckbau nicht gedroppt, sondern umbenannt und als historische
|
||||
-- Datenquelle aufbewahrt -- fuer eine spaetere Session, die z.B. pruefen
|
||||
-- will, wann/von wem eine bestimmte Rolle urspruenglich direkt vergeben
|
||||
-- wurde, bevor Migration 0017 sie gespiegelt hat. Der zugehoerige Index
|
||||
-- (0001_initial.sql:94) bleibt ueber die Umbenennung hinweg automatisch an
|
||||
-- die Tabelle gebunden -- SQLite aktualisiert die interne Referenz.
|
||||
--
|
||||
-- Keine FK verweist auf user_hostgroup_roles (sie ist ein reines Blatt im
|
||||
-- Schema), daher genuegt eine einfache Umbenennung ohne Neuaufbau-Tanz.
|
||||
|
||||
ALTER TABLE user_hostgroup_roles RENAME TO user_hostgroup_roles_legacy;
|
||||
22
app/db/migrations/0020_session_user_ended_index.sql
Normal file
22
app/db/migrations/0020_session_user_ended_index.sql
Normal file
@ -0,0 +1,22 @@
|
||||
-- Migration 0020: Index fuer die eigene-Sitzungen-Abfrage (Teil F.3.6,
|
||||
-- Umsetzungsauftrag_Sonnet5.md): GET /catalog/sessions (app/catalog/
|
||||
-- routes.py) filtert immer nach user_id UND (im Standardfall
|
||||
-- active_only=true) zusaetzlich nach ended_at IS NULL.
|
||||
--
|
||||
-- Bisher existierte nur idx_sessions_user (user_id allein,
|
||||
-- app/db/migrations/0001_initial.sql:107) -- fuer eine wachsende
|
||||
-- sessions-Tabelle waere "meine noch offenen Sitzungen" damit ein
|
||||
-- Index-Scan ueber ALLE Sitzungen dieses Benutzers mit anschliessendem
|
||||
-- Filter auf ended_at, statt direkt auf die (typischerweise sehr kleine)
|
||||
-- Teilmenge der offenen Sitzungen zuzugreifen.
|
||||
--
|
||||
-- Ein zusammengesetzter Index (user_id, ended_at) deckt per
|
||||
-- SQLite-Index-Praefix-Regel sowohl "alle meine Sitzungen" (user_id
|
||||
-- allein) als auch "meine offenen Sitzungen" (user_id + ended_at) ab und
|
||||
-- macht idx_sessions_user damit redundant -- bewusst NICHT geloescht
|
||||
-- (Projektkonvention: Indizes/Tabellen werden beim Rueckbau nicht
|
||||
-- gedroppt, siehe 0019_retire_direct_role_grants.sql; ausserdem nutzen
|
||||
-- andere Abfragen im Adminbereich, z.B. GET /admin/sessions, weiterhin
|
||||
-- ausschliesslich user_id ueber JOINs, nicht ueber diesen Index).
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user_ended ON sessions(user_id, ended_at);
|
||||
Reference in New Issue
Block a user