feat: global notification policy severity to channels (notif-22)

Singleton notification_policy table, NOTIFY_MIN_SEVERITY/CHANNELS env,
central dispatch gate, Settings policy UI, migration 007.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-05-29 16:19:26 +10:00
parent 9b177c145b
commit ebb450be92
19 changed files with 577 additions and 126 deletions
+63
View File
@@ -0,0 +1,63 @@
"""Global notification policy (notif-22)."""
from app.config import get_settings
from app.models.notification_policy import NotificationPolicy
from app.services.notification_policy import get_effective_notification_policy, upsert_notification_policy
from app.services.notification_severity import severity_meets_minimum
def test_severity_meets_minimum():
assert severity_meets_minimum("warning", "warning")
assert severity_meets_minimum("high", "warning")
assert not severity_meets_minimum("info", "warning")
def test_put_policy_persists_db(jwt_headers, client, db_session, monkeypatch):
monkeypatch.setenv("NOTIFY_MIN_SEVERITY", "high")
monkeypatch.setenv("NOTIFY_CHANNELS", "telegram")
get_settings.cache_clear()
response = client.put(
"/api/v1/settings/notifications/policy",
headers=jwt_headers,
json={
"min_severity": "warning",
"use_telegram": True,
"use_webhook": True,
"use_email": False,
},
)
assert response.status_code == 200
body = response.json()
assert body["min_severity"] == "warning"
assert body["use_webhook"] is True
assert body["source"] == "db"
row = db_session.get(NotificationPolicy, 1)
assert row is not None
assert row.use_webhook is True
cfg = get_effective_notification_policy(db_session)
assert cfg.min_severity == "warning"
def test_upsert_policy_syncs_channel_min_severity(db_session, monkeypatch):
monkeypatch.setenv("NOTIFY_MIN_SEVERITY", "high")
get_settings.cache_clear()
from app.models.notification_channel import NotificationChannel
db_session.add(
NotificationChannel(channel="telegram", enabled=False, min_severity="high")
)
db_session.commit()
upsert_notification_policy(
db_session,
min_severity="critical",
use_telegram=True,
use_webhook=False,
use_email=False,
)
row = db_session.get(NotificationChannel, "telegram")
assert row.min_severity == "critical"
+62
View File
@@ -0,0 +1,62 @@
"""notify_dispatch respects global policy."""
from datetime import datetime, timezone
from unittest.mock import patch
from app.models import Event
from app.services import notify_dispatch
from app.services.notification_policy import NotificationPolicyConfig
def test_notify_event_skipped_below_policy_severity():
event = Event(
event_id="00000000-0000-4000-8000-000000000401",
host_id=1,
occurred_at=datetime(2026, 5, 29, 15, 0, tzinfo=timezone.utc),
category="auth",
type="rdp.login.success",
severity="info",
title="ok",
summary="skip",
payload={},
)
policy = NotificationPolicyConfig(
min_severity="warning",
use_telegram=True,
use_webhook=False,
use_email=False,
source="db",
)
with patch.object(notify_dispatch, "get_effective_notification_policy", return_value=policy):
with patch.object(notify_dispatch, "telegram_notify") as mock_tg:
notify_dispatch.notify_event(event)
mock_tg.notify_event.assert_not_called()
def test_notify_event_calls_selected_channels():
event = Event(
event_id="00000000-0000-4000-8000-000000000402",
host_id=1,
occurred_at=datetime(2026, 5, 29, 15, 0, tzinfo=timezone.utc),
category="auth",
type="rdp.login.failed",
severity="warning",
title="fail",
summary="send",
payload={},
)
policy = NotificationPolicyConfig(
min_severity="warning",
use_telegram=True,
use_webhook=True,
use_email=False,
source="db",
)
with patch.object(notify_dispatch, "get_effective_notification_policy", return_value=policy):
with patch.object(notify_dispatch, "telegram_notify") as mock_tg:
with patch.object(notify_dispatch, "webhook_notify") as mock_wh:
with patch.object(notify_dispatch, "email_notify") as mock_em:
notify_dispatch.notify_event(event)
mock_tg.notify_event.assert_called_once()
mock_wh.notify_event.assert_called_once()
mock_em.notify_event.assert_not_called()
+2 -5
View File
@@ -22,6 +22,8 @@ def test_get_notification_settings_masks_secrets(jwt_headers, client, monkeypatc
assert body["telegram"]["min_severity"] == "warning"
assert body["telegram"]["source"] == "env"
assert body["telegram"]["bot_token_hint"].endswith("ghij")
assert "policy" in body
assert body["policy"]["min_severity"] == "warning"
assert "webhook" in body
assert body["webhook"]["enabled"] is False
assert "email" in body
@@ -38,7 +40,6 @@ def test_put_email_settings_persists_db(jwt_headers, client, db_session, monkeyp
headers=jwt_headers,
json={
"enabled": True,
"min_severity": "warning",
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"smtp_user": "monitor",
@@ -95,7 +96,6 @@ def test_put_webhook_settings_persists_db(jwt_headers, client, db_session, monke
headers=jwt_headers,
json={
"enabled": True,
"min_severity": "warning",
"url": "https://example.com/hooks/sac",
"secret_header": "X-SAC-Token",
"secret": "supersecret",
@@ -145,7 +145,6 @@ def test_put_telegram_settings_persists_db(jwt_headers, client, db_session, monk
headers=jwt_headers,
json={
"enabled": True,
"min_severity": "warning",
"bot_token": "1234567890:TESTTOKEN",
"chat_id": "999001",
},
@@ -154,7 +153,6 @@ def test_put_telegram_settings_persists_db(jwt_headers, client, db_session, monk
body = response.json()
assert body["source"] == "db"
assert body["enabled"] is True
assert body["min_severity"] == "warning"
row = db_session.get(NotificationChannel, "telegram")
assert row is not None
@@ -163,7 +161,6 @@ def test_put_telegram_settings_persists_db(jwt_headers, client, db_session, monk
cfg = get_effective_telegram_config(db_session)
assert cfg.source == "db"
assert cfg.min_severity == "warning"
def test_post_telegram_test_success(jwt_headers, client, db_session, monkeypatch):