umbau 1.0

This commit is contained in:
2026-09-02 20:30:44 +02:00
parent afe6719f51
commit 5c95b21be7
77 changed files with 10733 additions and 1914 deletions

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

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

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

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

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

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

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

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