"""Synchronisation von Azure AD Gruppenmitgliedern → CardSync User-Tabelle. Holt alle Mitglieder einer Azure AD Gruppe via Microsoft Graph (Application Permissions) und legt sie als User in CardSync an. Bestehende User werden aktualisiert, nicht mehr in der Gruppe vorhandene User werden NICHT automatisch gelöscht (nur deren Sync deaktiviert, damit es kein Datenverlust gibt). """ import logging from datetime import datetime, timezone from sqlalchemy.orm import Session from models import User, AzureGroupConfig from ms_graph import get_group_info, get_group_members logger = logging.getLogger(__name__) async def sync_group_members(db: Session) -> dict: """Holt alle Gruppenmitglieder und legt User in der DB an / aktualisiert sie.""" config = db.query(AzureGroupConfig).first() if not config: return {"status": "error", "message": "Keine Azure-Gruppen-Konfiguration vorhanden"} started = datetime.now(timezone.utc) try: # Gruppen-Info aktualisieren group_info = await get_group_info(config.group_id) if group_info: config.group_name = group_info.get("displayName", config.group_name) members = await get_group_members(config.group_id) logger.info(f"Azure AD: {len(members)} Mitglieder in Gruppe '{config.group_name}' gefunden") created = 0 updated = 0 deactivated = 0 # Set für die Erkennung entfernter Mitglieder current_member_ids = set() for m in members: ms_user_id = m.get("id") email = (m.get("mail") or m.get("userPrincipalName") or "").lower() display_name = m.get("displayName", "") if not ms_user_id or not email: continue current_member_ids.add(ms_user_id) user = db.query(User).filter(User.ms_user_id == ms_user_id).first() if not user: # Mit E-Mail als Fallback suchen user = db.query(User).filter(User.email == email).first() if user: # Existierender User → Stammdaten aktualisieren if user.ms_user_id != ms_user_id: user.ms_user_id = ms_user_id if user.email != email: user.email = email if user.display_name != display_name and display_name: user.display_name = display_name # Auto-Aktivierung NUR für reine Group-Import-User # OAuth-User behalten ihre individuellen Einstellungen if user.source == "group_import": if not user.sync_enabled and config.default_sync_enabled: user.sync_enabled = True user.updated_at = datetime.now(timezone.utc) updated += 1 else: # Neuer User new_user = User( ms_user_id=ms_user_id, email=email, display_name=display_name, source="group_import", sync_enabled=config.default_sync_enabled, carddav_folder=config.default_carddav_folder, sync_interval_minutes=config.default_sync_interval_minutes, ) db.add(new_user) created += 1 logger.info(f"Neuer User aus Gruppe importiert: {email}") # User die nicht mehr in der Gruppe sind → Sync deaktivieren # (nur für group_import User, OAuth-User nicht anfassen) all_imported = db.query(User).filter(User.source == "group_import").all() for u in all_imported: if u.ms_user_id not in current_member_ids and u.sync_enabled: u.sync_enabled = False u.last_sync_message = "Aus Azure AD Gruppe entfernt — Sync automatisch deaktiviert" u.updated_at = datetime.now(timezone.utc) deactivated += 1 logger.info(f"Sync deaktiviert für {u.email} (nicht mehr in Gruppe)") config.last_member_sync_at = started config.last_member_sync_status = "success" config.last_member_sync_message = ( f"{created} neu, {updated} aktualisiert, {deactivated} deaktiviert" ) db.commit() return { "status": "success", "message": config.last_member_sync_message, "created": created, "updated": updated, "deactivated": deactivated, "total_members": len(members), } except Exception as e: logger.error(f"Group member sync failed: {e}", exc_info=True) config.last_member_sync_at = started config.last_member_sync_status = "error" config.last_member_sync_message = str(e) db.commit() return {"status": "error", "message": str(e)}