more admin stuff
This commit is contained in:
97
app/tenancy.py
Normal file
97
app/tenancy.py
Normal 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}
|
||||
Reference in New Issue
Block a user