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,)
|