Harden auth and outbound admin surfaces

This commit is contained in:
2026-05-23 21:12:45 +12:00
parent d9ac54a2ff
commit 1ce01ec348
15 changed files with 495 additions and 110 deletions
+32 -5
View File
@@ -17,6 +17,7 @@ from ..clients.radarr import RadarrClient
from ..clients.sonarr import SonarrClient
from ..config import settings as env_settings
from ..db import get_database_diagnostics
from ..network_security import validate_notification_target_url
from ..runtime import get_runtime_settings
from .invite_email import send_test_email, smtp_email_config_ready, smtp_email_delivery_warning
@@ -97,7 +98,12 @@ def _config_status(detail: str) -> str:
def _discord_config_ready(runtime) -> tuple[bool, str]:
if not runtime.magent_notify_enabled or not runtime.magent_notify_discord_enabled:
return False, "Discord notifications are disabled."
if _clean_text(runtime.magent_notify_discord_webhook_url) or _clean_text(runtime.discord_webhook_url):
webhook_url = _clean_text(runtime.magent_notify_discord_webhook_url) or _clean_text(runtime.discord_webhook_url)
if webhook_url:
try:
validate_notification_target_url(webhook_url)
except ValueError as exc:
return False, str(exc)
return True, "ok"
return False, "Discord webhook URL is required."
@@ -113,7 +119,12 @@ def _telegram_config_ready(runtime) -> tuple[bool, str]:
def _webhook_config_ready(runtime) -> tuple[bool, str]:
if not runtime.magent_notify_enabled or not runtime.magent_notify_webhook_enabled:
return False, "Generic webhook notifications are disabled."
if _clean_text(runtime.magent_notify_webhook_url):
webhook_url = _clean_text(runtime.magent_notify_webhook_url)
if webhook_url:
try:
validate_notification_target_url(webhook_url)
except ValueError as exc:
return False, str(exc)
return True, "ok"
return False, "Generic webhook URL is required."
@@ -123,11 +134,21 @@ def _push_config_ready(runtime) -> tuple[bool, str]:
return False, "Push notifications are disabled."
provider = _clean_text(runtime.magent_notify_push_provider, "ntfy").lower()
if provider == "ntfy":
if _clean_text(runtime.magent_notify_push_base_url) and _clean_text(runtime.magent_notify_push_topic):
push_url = _clean_text(runtime.magent_notify_push_base_url)
if push_url and _clean_text(runtime.magent_notify_push_topic):
try:
validate_notification_target_url(push_url)
except ValueError as exc:
return False, str(exc)
return True, "ok"
return False, "ntfy requires a base URL and topic."
if provider == "gotify":
if _clean_text(runtime.magent_notify_push_base_url) and _clean_text(runtime.magent_notify_push_token):
push_url = _clean_text(runtime.magent_notify_push_base_url)
if push_url and _clean_text(runtime.magent_notify_push_token):
try:
validate_notification_target_url(push_url)
except ValueError as exc:
return False, str(exc)
return True, "ok"
return False, "Gotify requires a base URL and app token."
if provider == "pushover":
@@ -135,7 +156,12 @@ def _push_config_ready(runtime) -> tuple[bool, str]:
return True, "ok"
return False, "Pushover requires an application token and user key."
if provider == "webhook":
if _clean_text(runtime.magent_notify_push_base_url):
push_url = _clean_text(runtime.magent_notify_push_base_url)
if push_url:
try:
validate_notification_target_url(push_url)
except ValueError as exc:
return False, str(exc)
return True, "ok"
return False, "Webhook relay requires a target URL."
if provider == "telegram":
@@ -190,6 +216,7 @@ async def _run_http_post(
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
validate_notification_target_url(url)
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
response = await client.post(url, json=json_payload, data=data_payload, params=params, headers=headers)
response.raise_for_status()
+4
View File
@@ -8,6 +8,7 @@ import httpx
from ..config import settings as env_settings
from ..db import get_setting
from ..network_security import validate_notification_target_url
from ..runtime import get_runtime_settings
from .invite_email import send_generic_email
@@ -49,6 +50,7 @@ def _portal_item_url(item_id: int) -> str:
async def _http_post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
validate_notification_target_url(url)
async with httpx.AsyncClient(timeout=12.0) as client:
response = await client.post(url, json=payload)
response.raise_for_status()
@@ -115,6 +117,7 @@ async def _send_push(title: str, message: str, payload: Dict[str, Any]) -> Dict[
if provider == "ntfy":
if not base_url or not topic:
return {"status": "skipped", "detail": "ntfy needs base URL and topic."}
validate_notification_target_url(base_url)
url = f"{base_url.rstrip('/')}/{quote(topic)}"
headers = {"Title": title, "Tags": "magent,portal"}
async with httpx.AsyncClient(timeout=12.0) as client:
@@ -124,6 +127,7 @@ async def _send_push(title: str, message: str, payload: Dict[str, Any]) -> Dict[
if provider == "gotify":
if not base_url or not token:
return {"status": "skipped", "detail": "Gotify needs base URL and token."}
validate_notification_target_url(base_url)
url = f"{base_url.rstrip('/')}/message?token={quote(token)}"
body = {"title": title, "message": message, "priority": 5, "extras": {"client::display": {"contentType": "text/plain"}}}
result = await _http_post_json(url, body)