umbau 1.0
This commit is contained in:
99
scripts/promote_tenant_admins.py
Normal file
99
scripts/promote_tenant_admins.py
Normal file
@ -0,0 +1,99 @@
|
||||
#!/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))
|
||||
Reference in New Issue
Block a user