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

97
_to_delete/tenancy.py Normal file
View File

@ -0,0 +1,97 @@
"""
Mandanten-Scoping: zentrale Stelle, die die Mandanten-Sichtbarkeit eines
Principals (Super-Admin / Mandanten-Admin-Session / Mandanten-gebundenes
API-Token, siehe app/auth/deps.py) in SQL-WHERE-Fragmente und
Zugriffspruefungen uebersetzt. JEDER /admin-Endpunkt, der mandantengebundene
Ressourcen (Hostgruppen, Hosts, Benutzergruppen, SSH-Keys, API-Tokens,
Rollenvergaben, Audit-Log) liest oder schreibt, muss ueber TenantScope gehen
-- damit die Isolation an genau einer Stelle durchgesetzt wird statt in jedem
Endpunkt einzeln neu erfunden zu werden.
Sicherheitsprinzip: eine Zugriffsverletzung ueber eine ID aus einem fremden
Mandanten liefert 404 (nicht 403) -- ein Mandanten-Admin soll aus der
Fehlerantwort nicht einmal ableiten koennen, DASS eine ID in einem anderen
Mandanten existiert.
"""
from __future__ import annotations
import aiosqlite
from fastapi import HTTPException, status
class TenantScope:
def __init__(self, tenant_ids: set[int] | None):
# tenant_ids is None -> Super-Admin, keine Einschraenkung.
# tenant_ids ist Set -> nur diese Mandanten sichtbar/aenderbar
# (leeres Set == gar kein Mandant -> nichts sichtbar).
self.all_tenants = tenant_ids is None
self.tenant_ids: set[int] = set(tenant_ids) if tenant_ids else set()
def sql_filter(self, column: str) -> tuple[str, tuple]:
"""SQL-Fragment (mit fuehrendem ' AND ...') + Parameter-Tuple, das an
eine bestehende WHERE-Klausel angehaengt werden kann."""
if self.all_tenants:
return "", ()
if not self.tenant_ids:
return " AND 0", ()
placeholders = ",".join("?" for _ in self.tenant_ids)
return f" AND {column} IN ({placeholders})", tuple(self.tenant_ids)
def check(self, tenant_id: int | None) -> None:
"""Wirft 404, wenn tenant_id ausserhalb des erlaubten Bereichs liegt."""
if self.all_tenants:
return
if tenant_id is None or tenant_id not in self.tenant_ids:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Nicht gefunden")
def single_tenant_id(self) -> int | None:
"""Fuer Endpunkte, die zwingend GENAU einen Mandanten brauchen (z.B.
beim Anlegen einer neuen Hostgruppe). None bei Super-Admin (muss dann
explizit im Request mitgegeben werden), sonst der einzige erlaubte
Mandant -- oder ein 400/409, falls der Aufrufer mehrere hat (in dieser
Version hat ein Mandanten-Admin praktisch immer genau einen Mandanten,
die Datenstruktur erlaubt aber theoretisch mehrere)."""
if self.all_tenants or len(self.tenant_ids) != 1:
return None
return next(iter(self.tenant_ids))
async def resolve_host_group_tenant(conn: aiosqlite.Connection, host_group_id: int) -> int | None:
cursor = await conn.execute("SELECT tenant_id FROM host_groups WHERE id = ?", (host_group_id,))
row = await cursor.fetchone()
return row[0] if row else None
async def resolve_host_tenant(conn: aiosqlite.Connection, host_id: int) -> int | None:
cursor = await conn.execute(
"SELECT hg.tenant_id FROM hosts h JOIN host_groups hg ON hg.id = h.host_group_id WHERE h.id = ?",
(host_id,),
)
row = await cursor.fetchone()
return row[0] if row else None
async def tenant_user_ids(conn: aiosqlite.Connection, tenant_id: int) -> set[int]:
"""Alle Benutzer, die zu diesem Mandanten gehoeren: Mandanten-Admins,
Benutzer mit home_tenant_id = tenant_id, Mitglieder einer Benutzergruppe
dieses Mandanten, sowie Benutzer mit einer direkten Rollenvergabe auf
einer Hostgruppe dieses Mandanten. Wird sowohl fuer die Benutzer-Liste
einer Mandanten-Admin-Sicht als auch fuer die Audit-Log-Filterung genutzt."""
cursor = await conn.execute(
"""
SELECT user_id FROM tenant_admins WHERE tenant_id = ?
UNION
SELECT id FROM users WHERE home_tenant_id = ?
UNION
SELECT ugm.user_id FROM user_group_members ugm
JOIN user_groups ug ON ug.id = ugm.user_group_id
WHERE ug.tenant_id = ?
UNION
SELECT uhr.user_id FROM user_hostgroup_roles uhr
JOIN host_groups hg ON hg.id = uhr.host_group_id
WHERE hg.tenant_id = ?
""",
(tenant_id, tenant_id, tenant_id, tenant_id),
)
rows = await cursor.fetchall()
return {r[0] for r in rows}