100 lines
4.0 KiB
Python
100 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Einmaliger Datenmigrationsschritt fuer TEIL C, Phase 1 des
|
|
Umsetzungsauftrag_Sonnet5.md ("Rollenmodell bereinigen").
|
|
|
|
Betreiberentscheidung (Phase 0.2): jeder Benutzer mit einer Zeile in
|
|
tenant_admins wird zu is_admin=1 befoerdert (Voll-Admin), STATT zum
|
|
normalen Benutzer degradiert zu werden. Muss AUSGEFUEHRT WERDEN, BEVOR die
|
|
Phase-2-Codeaenderungen (TenantScope-Entfernung) deployt werden -- siehe
|
|
C.4: wird TenantScope entfernt, ohne vorher jeden tenant_admins-Eintrag
|
|
explizit auf is_admin=1 zu heben, waere die Rechteausweitung fuer diese
|
|
Benutzer zwar am Ende dieselbe, aber implizit und ohne Audit-Spur zum
|
|
Zeitpunkt der eigentlichen Entscheidung. Nach diesem Schritt verhaelt sich
|
|
die Anwendung noch exakt wie vorher (Phase 1 ist bewusst noch KEINE
|
|
Codeaenderung) -- ein bereits is_admin=1 gesetzter Benutzer sieht/kann
|
|
nichts anderes als vorher, ein bisheriger reiner Mandanten-Admin sieht ab
|
|
sofort (noch ueber den unveraenderten is_any_admin-Pfad) alle Mandanten
|
|
statt nur seiner/seine eigenen. Das ist die in Phase 0.2 getroffene,
|
|
gewollte Entscheidung -- keine Nebenwirkung.
|
|
|
|
Idempotent: bereits befoerderte oder inzwischen geloeschte/deaktivierte
|
|
Benutzer werden uebersprungen; mehrfacher Aufruf ist gefahrlos.
|
|
|
|
Aufruf (vor dem Wartungsfenster aus Phase 2, mit noch unveraendertem Code):
|
|
<venv>/bin/python scripts/promote_tenant_admins.py [--dry-run]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from app.db import close_db, get_db, init_db # noqa: E402
|
|
from app.security.audit import write_audit_event # noqa: E402
|
|
|
|
|
|
async def main(dry_run: bool) -> None:
|
|
await init_db()
|
|
conn = get_db()
|
|
try:
|
|
cursor = await conn.execute(
|
|
"SELECT DISTINCT ta.user_id, u.username, u.is_admin "
|
|
"FROM tenant_admins ta JOIN users u ON u.id = ta.user_id "
|
|
"WHERE u.deleted_at IS NULL "
|
|
"ORDER BY u.username"
|
|
)
|
|
rows = await cursor.fetchall()
|
|
|
|
if not rows:
|
|
print("Keine tenant_admins-Eintraege vorhanden -- nichts zu tun.")
|
|
return
|
|
|
|
to_promote = [(uid, uname) for uid, uname, is_admin in rows if not is_admin]
|
|
already_admin = [uname for _uid, uname, is_admin in rows if is_admin]
|
|
|
|
print(f"Gefunden: {len(rows)} Benutzer mit mind. einer tenant_admins-Zeile.")
|
|
if already_admin:
|
|
print(f" Bereits is_admin=1 (unveraendert): {', '.join(already_admin)}")
|
|
if not to_promote:
|
|
print("Alle betroffenen Benutzer sind bereits is_admin=1 -- nichts zu tun.")
|
|
return
|
|
|
|
print(f" Wird auf is_admin=1 befoerdert: {', '.join(u for _i, u in to_promote)}")
|
|
if dry_run:
|
|
print("--dry-run: keine Aenderung vorgenommen.")
|
|
return
|
|
|
|
for user_id, username in to_promote:
|
|
await conn.execute("UPDATE users SET is_admin = 1 WHERE id = ?", (user_id,))
|
|
await write_audit_event(
|
|
conn,
|
|
event_type="tenant_admin_promoted_to_admin",
|
|
user_id=user_id,
|
|
client_ip=None,
|
|
details={
|
|
"reason": (
|
|
"Umsetzungsauftrag_Sonnet5.md Teil C, Phase 0.2/1: "
|
|
"Rueckbau der Mandantenfaehigkeit -- bestehende "
|
|
"tenant_admins-Zuweisung wird zu vollem is_admin=1 "
|
|
"befoerdert (Betreiberentscheidung, keine Degradierung)."
|
|
),
|
|
"username": username,
|
|
},
|
|
)
|
|
await conn.commit()
|
|
print(f"{len(to_promote)} Benutzer befoerdert, je ein Audit-Event geschrieben.")
|
|
finally:
|
|
await close_db()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--dry-run", action="store_true", help="Nur anzeigen, was passieren wuerde, nichts schreiben."
|
|
)
|
|
args = parser.parse_args()
|
|
asyncio.run(main(args.dry_run))
|