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>
340 lines
9.3 KiB
Python
340 lines
9.3 KiB
Python
"""Tests for RDG 302→303 session flap rule."""
|
|
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from app.config import get_settings
|
|
from app.models import Problem
|
|
from app.services.ingest import ingest_event
|
|
from app.services.problems import maybe_create_problem
|
|
from app.services.problem_rules import RULE_RDG_SESSION_FLAP
|
|
from app.services.rdg_session_flap import (
|
|
evaluate_rdg_session_flap,
|
|
event_has_rdg_flap,
|
|
find_rdg_success_before_end,
|
|
resolve_rdg_flap_summary,
|
|
resolve_rdg_qwinsta_enabled,
|
|
)
|
|
from app.services.event_summary import event_to_summary
|
|
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")
|
|
get_settings.cache_clear()
|
|
yield
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def test_rdg_flap_detects_302_then_303(db_session, rdg_settings):
|
|
t0 = datetime.now(timezone.utc)
|
|
user = "B26\\test.user"
|
|
details = {"user": user, "internal_ip": "192.168.163.32"}
|
|
|
|
_ingest(
|
|
db_session,
|
|
t0,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="RD Gateway event 302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=4),
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="RD Gateway event 303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
|
|
match = evaluate_rdg_session_flap(db_session, end)
|
|
assert match is not None
|
|
assert match.rule_id == RULE_RDG_SESSION_FLAP
|
|
assert event_has_rdg_flap(end) is True
|
|
|
|
problem, created = maybe_create_problem(db_session, end)
|
|
assert created is True
|
|
assert problem.rule_id == RULE_RDG_SESSION_FLAP
|
|
|
|
|
|
def test_rdg_flap_ignores_gap_over_10_sec(db_session, rdg_settings):
|
|
t0 = datetime.now(timezone.utc)
|
|
user = "B26\\slow.user"
|
|
details = {"user": user}
|
|
|
|
_ingest(
|
|
db_session,
|
|
t0,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=15),
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
|
|
assert find_rdg_success_before_end(db_session, end) is None
|
|
assert evaluate_rdg_session_flap(db_session, end) is None
|
|
|
|
|
|
def test_rdg_flap_dedup_within_30_sec(db_session, rdg_settings):
|
|
t0 = datetime.now(timezone.utc)
|
|
user = "B26\\dup.user"
|
|
details = {"user": user}
|
|
|
|
def pair(at: datetime):
|
|
_ingest(
|
|
db_session,
|
|
at,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
return _ingest(
|
|
db_session,
|
|
at + timedelta(seconds=5),
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
|
|
end1 = pair(t0)
|
|
maybe_create_problem(db_session, end1)
|
|
db_session.flush()
|
|
|
|
end2 = pair(t0 + timedelta(seconds=10))
|
|
maybe_create_problem(db_session, end2)
|
|
db_session.flush()
|
|
|
|
from sqlalchemy import func, select
|
|
|
|
open_count = db_session.scalar(
|
|
select(func.count()).select_from(Problem).where(
|
|
Problem.rule_id == RULE_RDG_SESSION_FLAP,
|
|
Problem.status == "open",
|
|
)
|
|
)
|
|
assert open_count == 1
|
|
|
|
|
|
def test_resolve_rdg_flap_summary_for_302_and_303(db_session, rdg_settings):
|
|
t0 = datetime.now(timezone.utc)
|
|
user = "B26\\pair.user"
|
|
details = {"user": user, "internal_ip": "192.168.163.48"}
|
|
|
|
start = _ingest(
|
|
db_session,
|
|
t0,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="warning",
|
|
title="RD Gateway event 302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=5),
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="RD Gateway event 303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
evaluate_rdg_session_flap(db_session, end)
|
|
db_session.flush()
|
|
|
|
end_flap, end_pair, end_qwinsta = resolve_rdg_flap_summary(db_session, end)
|
|
start_flap, start_pair, start_qwinsta = resolve_rdg_flap_summary(db_session, start)
|
|
|
|
assert end_flap is True
|
|
assert end_pair == start.id
|
|
assert end_qwinsta == start.id
|
|
|
|
assert start_flap is True
|
|
assert start_pair == end.id
|
|
assert start_qwinsta == start.id
|
|
|
|
end_summary = event_to_summary(end, db_session)
|
|
start_summary = event_to_summary(start, db_session)
|
|
assert end_summary.rdg_flap is True
|
|
assert start_summary.rdg_flap is True
|
|
assert start_summary.rdg_flap_qwinsta_event_id == start.id
|
|
assert start_summary.rdg_qwinsta_enabled is True
|
|
assert end_summary.rdg_qwinsta_enabled is False
|
|
|
|
|
|
def test_rdg_qwinsta_disabled_after_normal_session_end(db_session, rdg_settings):
|
|
t0 = datetime.now(timezone.utc)
|
|
user = "B26\\normal.user"
|
|
details = {"user": user, "internal_ip": "192.168.163.49"}
|
|
|
|
start = _ingest(
|
|
db_session,
|
|
t0,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(minutes=20),
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
|
|
assert resolve_rdg_qwinsta_enabled(db_session, start) is False
|
|
assert resolve_rdg_qwinsta_enabled(db_session, end) is False
|
|
|
|
start_summary = event_to_summary(start, db_session)
|
|
end_summary = event_to_summary(end, db_session)
|
|
assert start_summary.rdg_qwinsta_enabled is False
|
|
assert end_summary.rdg_qwinsta_enabled is False
|
|
|
|
|
|
def test_rdg_qwinsta_enabled_while_session_open(db_session, rdg_settings):
|
|
t0 = datetime.now(timezone.utc)
|
|
details = {"user": r"B26\active.user", "internal_ip": "192.168.163.50"}
|
|
|
|
start = _ingest(
|
|
db_session,
|
|
t0,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
|
|
assert resolve_rdg_qwinsta_enabled(db_session, start) is True
|
|
assert event_to_summary(start, db_session).rdg_qwinsta_enabled is True
|
|
|
|
|
|
def test_rdg_qwinsta_disabled_on_flap_302_after_later_success(db_session, rdg_settings):
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\m.semenova"
|
|
details = {"user": user, "internal_ip": "192.168.164.45"}
|
|
|
|
flap_start = _ingest(
|
|
db_session,
|
|
t0,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="warning",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
flap_end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=5),
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
evaluate_rdg_session_flap(db_session, flap_end)
|
|
db_session.flush()
|
|
|
|
assert resolve_rdg_qwinsta_enabled(db_session, flap_start) is True
|
|
|
|
_ingest(
|
|
db_session,
|
|
t0 + timedelta(minutes=2, seconds=44),
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="warning",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
|
|
assert resolve_rdg_qwinsta_enabled(db_session, flap_start) is False
|
|
assert event_to_summary(flap_start, db_session).rdg_qwinsta_enabled is False
|
|
|
|
|
|
def test_rdg_qwinsta_stays_enabled_when_unrelated_303_without_internal_ip(db_session, rdg_settings):
|
|
t0 = datetime.now(timezone.utc)
|
|
user = "B26\\khodasevich"
|
|
details_302 = {"user": user, "internal_ip": "192.168.160.209"}
|
|
details_303_other = {"user": user}
|
|
|
|
start = _ingest(
|
|
db_session,
|
|
t0,
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="warning",
|
|
title="302",
|
|
summary="302",
|
|
details=details_302,
|
|
)
|
|
_ingest(
|
|
db_session,
|
|
t0 + timedelta(minutes=5),
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details=details_303_other,
|
|
)
|
|
|
|
assert resolve_rdg_qwinsta_enabled(db_session, start) is True
|
|
assert event_to_summary(start, db_session).rdg_qwinsta_enabled is True
|