3765d4d476
Add optional auto logoff of stuck sessions on RDG flap and direct RDP failure. Hide the RDS break button on old flap 302 when the user later reconnects or the workstation session is closed. Co-authored-by: Cursor <cursoragent@cursor.com>
243 lines
7.6 KiB
Python
243 lines
7.6 KiB
Python
"""Tests for RDP flap auto-disconnect setting and service."""
|
|
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from app.config import get_settings
|
|
from app.models import Host
|
|
from app.models.ui_settings import UI_SETTINGS_ROW_ID, UiSettings
|
|
from app.services.ingest import ingest_event
|
|
from app.services.problems import maybe_create_problem
|
|
from app.services.rdp_flap_auto_disconnect import maybe_auto_disconnect_stuck_rdp_session
|
|
from app.services.rdp_flap_settings import get_effective_rdp_flap_settings, upsert_rdp_flap_settings
|
|
from app.services.winrm_connect import WinRmCmdResult
|
|
from tests.test_ingest import VALID_EVENT
|
|
|
|
|
|
def _payload(**overrides):
|
|
base = {
|
|
**VALID_EVENT,
|
|
"event_id": str(uuid.uuid4()),
|
|
"occurred_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def _ingest(db, occurred_at: datetime, **overrides):
|
|
payload = _payload(**overrides)
|
|
payload["occurred_at"] = occurred_at.isoformat()
|
|
event, _ = ingest_event(db, payload)
|
|
db.flush()
|
|
return event
|
|
|
|
|
|
@pytest.fixture
|
|
def rdg_settings(monkeypatch):
|
|
monkeypatch.setenv("SAC_RDG_FLAP_WINDOW_MIN_SEC", "1")
|
|
monkeypatch.setenv("SAC_RDG_FLAP_WINDOW_MAX_SEC", "10")
|
|
monkeypatch.setenv("SAC_RDG_FLAP_DEDUP_SEC", "30")
|
|
monkeypatch.setenv("SAC_WIN_ADMIN_USER", r"B26\admin")
|
|
monkeypatch.setenv("SAC_WIN_ADMIN_PASSWORD", "secret")
|
|
get_settings.cache_clear()
|
|
yield
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def rdg_hosts(db_session):
|
|
ws = Host(
|
|
hostname="TSA-PC",
|
|
os_family="windows",
|
|
product="rdp-login-monitor",
|
|
ipv4="192.168.163.100",
|
|
)
|
|
gw = Host(
|
|
hostname="K6A-DC3",
|
|
os_family="windows",
|
|
product="rdp-login-monitor",
|
|
ipv4="192.168.160.40",
|
|
)
|
|
db_session.add_all([ws, gw])
|
|
db_session.commit()
|
|
return ws, gw
|
|
|
|
|
|
def test_rdp_flap_settings_default_disabled(db_session):
|
|
cfg = get_effective_rdp_flap_settings(db_session)
|
|
assert cfg.auto_disconnect is False
|
|
assert cfg.source == "default"
|
|
|
|
|
|
def test_rdp_flap_settings_upsert(db_session):
|
|
upsert_rdp_flap_settings(db_session, auto_disconnect=True)
|
|
cfg = get_effective_rdp_flap_settings(db_session)
|
|
assert cfg.auto_disconnect is True
|
|
row = db_session.get(UiSettings, UI_SETTINGS_ROW_ID)
|
|
assert row is not None
|
|
assert row.auto_rdp_flap_disconnect is True
|
|
|
|
|
|
def test_auto_disconnect_skipped_when_disabled(db_session, rdg_settings, rdg_hosts, monkeypatch):
|
|
ws, gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\TSA"
|
|
details = {"user": user, "internal_ip": ws.ipv4}
|
|
gw_payload = {
|
|
"host": {"hostname": gw.hostname, "os_family": "windows", "ipv4": gw.ipv4},
|
|
"source": {"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
}
|
|
|
|
_ingest(
|
|
db_session,
|
|
t0,
|
|
**gw_payload,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=4),
|
|
**gw_payload,
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
maybe_create_problem(db_session, end)
|
|
|
|
mock_logoff = MagicMock()
|
|
monkeypatch.setattr("app.services.rdp_flap_auto_disconnect.execute_logoff_via_winrm", mock_logoff)
|
|
|
|
result = maybe_auto_disconnect_stuck_rdp_session(db_session, end)
|
|
assert result is None
|
|
mock_logoff.assert_not_called()
|
|
|
|
|
|
def test_auto_disconnect_rdg_flap_calls_logoff(db_session, rdg_settings, rdg_hosts, monkeypatch):
|
|
ws, gw = rdg_hosts
|
|
upsert_rdp_flap_settings(db_session, auto_disconnect=True)
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\TSA"
|
|
details = {"user": user, "internal_ip": ws.ipv4}
|
|
gw_payload = {
|
|
"host": {"hostname": gw.hostname, "os_family": "windows", "ipv4": gw.ipv4},
|
|
"source": {"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
}
|
|
|
|
_ingest(
|
|
db_session,
|
|
t0,
|
|
**gw_payload,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
_ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=1),
|
|
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": ws.ipv4},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="login",
|
|
summary="login",
|
|
details={"user": user},
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=4),
|
|
**gw_payload,
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
maybe_create_problem(db_session, end)
|
|
|
|
qwinsta_stdout = "SESSIONNAME USERNAME ID STATE\n rdp-tcp#0 B26\\TSA 5 Active\n"
|
|
monkeypatch.setattr(
|
|
"app.services.rdp_flap_auto_disconnect.list_windows_sessions",
|
|
lambda host, cfg: (
|
|
[],
|
|
WinRmCmdResult(ok=True, message="ok", target=host.ipv4 or "", stdout=qwinsta_stdout),
|
|
),
|
|
)
|
|
mock_logoff = MagicMock(return_value=SimpleNamespace(status="completed", result_stderr=None, result_stdout="ok"))
|
|
monkeypatch.setattr("app.services.rdp_flap_auto_disconnect.execute_logoff_via_winrm", mock_logoff)
|
|
|
|
result = maybe_auto_disconnect_stuck_rdp_session(db_session, end)
|
|
assert result is not None
|
|
assert result.ok is True
|
|
assert result.session_ids == (5,)
|
|
mock_logoff.assert_called_once()
|
|
assert end.details["rdp_flap_auto_disconnect"]["ok"] is True
|
|
|
|
|
|
def test_auto_disconnect_direct_rdp_failed(db_session, rdg_settings, rdg_hosts, monkeypatch):
|
|
ws, _gw = rdg_hosts
|
|
upsert_rdp_flap_settings(db_session, auto_disconnect=True)
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\TSA"
|
|
ws_payload = {
|
|
"host": {"hostname": ws.hostname, "os_family": "windows", "ipv4": ws.ipv4},
|
|
"source": {"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
}
|
|
|
|
_ingest(
|
|
db_session,
|
|
t0,
|
|
**ws_payload,
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="login",
|
|
summary="login",
|
|
details={"user": user},
|
|
)
|
|
failed = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=30),
|
|
**ws_payload,
|
|
type="rdp.login.failed",
|
|
category="auth",
|
|
severity="warning",
|
|
title="failed",
|
|
summary="failed",
|
|
details={"user": user},
|
|
)
|
|
|
|
qwinsta_stdout = "SESSIONNAME USERNAME ID STATE\n rdp-tcp#0 B26\\TSA 3 Active\n"
|
|
monkeypatch.setattr(
|
|
"app.services.rdp_flap_auto_disconnect.list_windows_sessions",
|
|
lambda host, cfg: (
|
|
[],
|
|
WinRmCmdResult(ok=True, message="ok", target=host.ipv4 or "", stdout=qwinsta_stdout),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.services.rdp_flap_auto_disconnect.terminate_windows_session",
|
|
lambda host, cfg, sid: WinRmCmdResult(ok=True, message="ok", target=host.ipv4 or ""),
|
|
)
|
|
|
|
result = maybe_auto_disconnect_stuck_rdp_session(db_session, failed)
|
|
assert result is not None
|
|
assert result.ok is True
|
|
assert result.session_ids == (3,)
|