"""RDG qwinsta/logoff via WinRM on client workstation (variant B).""" from __future__ import annotations import uuid from datetime import datetime, timezone from fastapi import HTTPException from sqlalchemy.orm import Session from app.models import AgentCommand, Event, Host from app.services.rdg_client_host import ClientWorkstationNotFoundError, resolve_client_workstation from app.services.rdg_session_flap import event_has_rdg_flap, event_internal_ip from app.services.win_admin_settings import get_effective_win_admin_config from app.services.winrm_connect import ( WinRmCmdResult, run_winrm_logoff, run_winrm_on_host_targets, run_winrm_qwinsta, ) def _require_win_admin(db: Session): cfg = get_effective_win_admin_config(db) if not cfg.configured: raise HTTPException( status_code=503, detail="Windows domain admin is not configured (Settings or SAC_WIN_ADMIN_*)", ) return cfg def _require_rdg_flap(event: Event) -> None: if not event_has_rdg_flap(event): raise HTTPException(status_code=400, detail="Event is not flagged as RDG session flap") def _resolve_client(db: Session, event: Event) -> Host: try: return resolve_client_workstation(db, event) except ClientWorkstationNotFoundError as exc: if not exc.internal_ip: raise HTTPException( status_code=400, detail="Event has no internal_ip (client workstation address)", ) from exc raise HTTPException(status_code=404, detail=str(exc)) from exc def _format_result_message(result: WinRmCmdResult, *, attempts: list[str]) -> str: message = result.message if attempts: message = f"{message} (пробовали: {', '.join(attempts)})" return message def _persist_command( db: Session, *, client_host: Host, event: Event, command_type: str, params: dict, requested_by: str, result: WinRmCmdResult, attempts: list[str], ) -> AgentCommand: now = datetime.now(timezone.utc) cmd = AgentCommand( command_uuid=str(uuid.uuid4()), host_id=client_host.id, event_id=event.id, command_type=command_type, params=params, status="completed" if result.ok else "failed", result_stdout=result.stdout or None, result_stderr=(result.stderr or _format_result_message(result, attempts=attempts) or None) if not result.ok else None, requested_by=requested_by, created_at=now, completed_at=now, ) db.add(cmd) db.flush() return cmd def execute_qwinsta_via_winrm(db: Session, event: Event, *, requested_by: str) -> AgentCommand: _require_rdg_flap(event) cfg = _require_win_admin(db) client_host = _resolve_client(db, event) details = event.details if isinstance(event.details, dict) else {} user = details.get("user") internal_ip = event_internal_ip(event) result, attempts = run_winrm_on_host_targets( client_host, user=cfg.user, password=cfg.password, action=lambda target: run_winrm_qwinsta(target=target, user=cfg.user, password=cfg.password), ) assert result is not None params = { "execution": "winrm", "user": user, "internal_ip": internal_ip, "client_hostname": client_host.hostname, "client_host_id": client_host.id, "winrm_target": result.target, } return _persist_command( db, client_host=client_host, event=event, command_type="qwinsta", params=params, requested_by=requested_by, result=result, attempts=attempts, ) def execute_logoff_via_winrm( db: Session, event: Event, *, session_id: int, requested_by: str, ) -> AgentCommand: _require_rdg_flap(event) cfg = _require_win_admin(db) client_host = _resolve_client(db, event) details = event.details if isinstance(event.details, dict) else {} user = details.get("user") internal_ip = event_internal_ip(event) result, attempts = run_winrm_on_host_targets( client_host, user=cfg.user, password=cfg.password, action=lambda target: run_winrm_logoff( target=target, user=cfg.user, password=cfg.password, session_id=session_id, ), ) assert result is not None params = { "execution": "winrm", "user": user, "internal_ip": internal_ip, "client_hostname": client_host.hostname, "client_host_id": client_host.id, "session_id": session_id, "winrm_target": result.target, } return _persist_command( db, client_host=client_host, event=event, command_type="logoff", params=params, requested_by=requested_by, result=result, attempts=attempts, )