Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 32 additions & 28 deletions src/wudup/web_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from __future__ import annotations

import argparse
import ipaddress
from collections.abc import Callable, Iterator, Sequence
from pathlib import Path
from types import SimpleNamespace

from fastapi import Request, Response

Expand Down Expand Up @@ -206,7 +206,7 @@ def _web_doctor_options_and_env(
settings: WebSettings,
) -> tuple[DoctorDataOptions, dict[str, str]]:
env = _doctor_command_env(settings)
args = SimpleNamespace(
args = argparse.Namespace(
base=str(settings.config.docker_base),
file=str(settings.config.wud_out_file),
log_dir=str(settings.config.log_dir),
Expand Down Expand Up @@ -259,13 +259,10 @@ def _doctor_command_env(settings: WebSettings) -> dict[str, str]:
return env


def _web_doctor_checks(
def _web_access_doctor_checks(
settings: WebSettings,
request: Request,
) -> tuple[DoctorDataCheck, ...]:
checks: list[DoctorDataCheck] = []
checks.append(_web_database_doctor_check(settings))
checks.append(
return (
_web_doctor_check(
"WARN" if settings.dev_no_auth else "PASS",
"WebUI authentication",
Expand All @@ -285,9 +282,7 @@ def _web_doctor_checks(
snippet="WUD_WEB_DEV_NO_AUTH=false",
),
),
)
)
checks.append(
),
_web_doctor_check(
"WARN" if settings.mutations_enabled else "PASS",
"WebUI mutation gate",
Expand All @@ -307,9 +302,7 @@ def _web_doctor_checks(
snippet="WUD_WEB_MUTATIONS_ENABLED=false",
),
),
)
)
checks.append(
),
_web_doctor_check(
"PASS" if settings.allowed_hosts else "FAIL",
"WebUI allowed hosts",
Expand All @@ -320,18 +313,21 @@ def _web_doctor_checks(
else (
DoctorDataSuggestion(
label="Configure allowed hosts",
description=(
"Set the hostnames clients use to reach the WebUI."
),
description="Set the hostnames clients use to reach the WebUI.",
snippet="WUD_WEB_ALLOWED_HOSTS=localhost,127.0.0.1",
),
),
)
),
)
checks.append(_web_wud_api_doctor_check(settings))
checks.extend(_web_wud_api_diagnostic_checks(settings))


def _web_request_security_doctor_checks(
settings: WebSettings,
request: Request,
) -> tuple[DoctorDataCheck, ...]:
effective_origin = _effective_origin(request, settings)
checks.append(
secure_cookie = _secure_cookie(settings, request)
return (
_web_doctor_check(
"PASS" if settings.public_origin else "WARN",
"WebUI public origin",
Expand All @@ -342,10 +338,7 @@ def _web_doctor_checks(
suggestions=()
if settings.public_origin
else _public_origin_suggestions(settings, request, effective_origin),
)
)
secure_cookie = _secure_cookie(settings, request)
checks.append(
),
_web_doctor_check(
"PASS" if secure_cookie else "WARN",
"WebUI secure cookies",
Expand All @@ -363,17 +356,28 @@ def _web_doctor_checks(
snippet="WUD_WEB_PUBLIC_ORIGIN=https://wud.example.test",
),
),
)
)
checks.append(
),
_web_doctor_check(
"PASS",
"WebUI trusted proxies",
_format_sequence(str(network) for network in settings.trusted_proxies)
or "not configured",
code="webui-trusted-proxies",
)
),
)


def _web_doctor_checks(
settings: WebSettings,
request: Request,
) -> tuple[DoctorDataCheck, ...]:
checks = [
_web_database_doctor_check(settings),
*_web_access_doctor_checks(settings),
_web_wud_api_doctor_check(settings),
]
checks.extend(_web_wud_api_diagnostic_checks(settings))
checks.extend(_web_request_security_doctor_checks(settings, request))
static_available = _static_spa_available(settings)
checks.append(
_web_doctor_check(
Expand Down
47 changes: 47 additions & 0 deletions tests/test_python_web_health.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

import argparse
import json
from pathlib import Path

from wudup import web as web_module
from wudup import web_health

from tests.web_test_helpers import (
WUD_API_AUTH_CONFIG_KEY,
Expand Down Expand Up @@ -185,6 +187,51 @@ def test_doctor_endpoint_enforces_auth_csrf_and_post(
assert get_response.status_code == 405
assert get_response.headers["allow"] == "POST"

def test_doctor_endpoint_preserves_webui_safety_check_contract(
tmp_path: Path,
monkeypatch,
) -> None:
namespaces: list[argparse.Namespace] = []
options_from_namespace = web_health.doctor_options_from_namespace

def track_namespace(args, **kwargs):
namespaces.append(args)
return options_from_namespace(args, **kwargs)

monkeypatch.setattr(
web_health,
"doctor_options_from_namespace",
track_namespace,
)
client = _doctor_client(
tmp_path,
{"WUD_WEB_MUTATIONS_ENABLED": "true"},
)

response = client.post("/api/v1/doctor", headers=_csrf_headers(client))
checks = {check["code"]: check for check in response.json()["checks"]}

assert response.status_code == 200
assert len(namespaces) == 1
assert isinstance(namespaces[0], argparse.Namespace)
assert {
"webui-authentication",
"webui-mutation-gate",
"webui-allowed-hosts",
"webui-public-origin",
"webui-secure-cookies",
"webui-trusted-proxies",
"webui-static-spa",
}.issubset(checks)
assert checks["webui-authentication"]["status"] == "WARN"
assert checks["webui-authentication"]["suggestions"][0]["snippet"] == (
"WUD_WEB_DEV_NO_AUTH=false"
)
assert checks["webui-mutation-gate"]["status"] == "WARN"
assert checks["webui-mutation-gate"]["suggestions"][0]["snippet"] == (
"WUD_WEB_MUTATIONS_ENABLED=false"
)

def test_doctor_endpoint_returns_structured_redacted_results(
tmp_path: Path,
) -> None:
Expand Down