fix: fail closed on an unwritable audit log, bound the rate limiter and harden startup
The audit logger is opened at startup (exit 2 on failure) and obtained before every call, which is refused with audit_unavailable otherwise. Audit records only take validated path and sudo values and use the request id as correlation id. The rate limiter evicts idle keys, caps tracked keys, audits the first excess per key and window and uses the settings passed to the app. FORWARDED_ALLOW_IPS is now a setting, and startup warns about a missing host allow-list and unavailable OAuth.
This commit is contained in:
1 parent
a3222d5ebe
commit
cf3d2e17d3
16 files changed
+532
-115
No files matched your search
@@ -95,14 +95,107 @@ def test_loopback_without_auth_runs(uvicorn_spy: UvicornSpy) -> None:
|
||||
assert uvicorn_spy.calls[0][1]["log_level"] == "debug"
|
||||
|
||||
|
||||
def test_forwarded_allow_ips_setting_is_honoured_when_present(
|
||||
uvicorn_spy: UvicornSpy, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(Settings, "forwarded_allow_ips", "10.0.0.0/8", raising=False)
|
||||
def test_forwarded_allow_ips_setting_is_passed_to_uvicorn(uvicorn_spy: UvicornSpy) -> None:
|
||||
assert run_http(server_settings(forwarded_allow_ips="10.0.0.0/8, 172.18.0.2")) == 0
|
||||
|
||||
assert uvicorn_spy.calls[0][1]["forwarded_allow_ips"] == "10.0.0.0/8,172.18.0.2"
|
||||
|
||||
|
||||
def test_forwarded_allow_ips_wildcard_needs_an_explicit_value_and_warns(
|
||||
uvicorn_spy: UvicornSpy, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
assert server_settings().forwarded_allow_ips == "127.0.0.1"
|
||||
|
||||
assert run_http(server_settings(forwarded_allow_ips="*")) == 0
|
||||
|
||||
assert uvicorn_spy.calls[0][1]["forwarded_allow_ips"] == "*"
|
||||
assert "FORWARDED_ALLOW_IPS=*" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_non_loopback_bind_without_public_host_warns_which_variables_to_set(
|
||||
uvicorn_spy: UvicornSpy, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
assert run_http(server_settings(mcp_host=ANY_HOST, mcp_bearer_token=BEARER_TOKEN)) == 0
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "PUBLIC_BASE_URL" in err and "ALLOWED_HOSTS" in err and "421" in err
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"extra",
|
||||
[{"public_base_url": "https://mcp.example.com"}, {"allowed_hosts": ["mcp.example.com"]}],
|
||||
)
|
||||
def test_no_host_warning_when_a_public_host_is_configured(
|
||||
uvicorn_spy: UvicornSpy, capsys: pytest.CaptureFixture[str], extra: dict[str, Any]
|
||||
) -> None:
|
||||
settings = server_settings(mcp_host=ANY_HOST, mcp_bearer_token=BEARER_TOKEN, **extra)
|
||||
|
||||
assert run_http(settings) == 0
|
||||
|
||||
assert "421" not in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_no_host_warning_on_loopback(
|
||||
uvicorn_spy: UvicornSpy, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
assert run_http(server_settings()) == 0
|
||||
|
||||
assert uvicorn_spy.calls[0][1]["forwarded_allow_ips"] == "10.0.0.0/8"
|
||||
assert capsys.readouterr().err == ""
|
||||
|
||||
|
||||
def test_oauth_enabled_with_a_bearer_warns_that_oauth_is_unavailable(
|
||||
uvicorn_spy: UvicornSpy, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
assert run_http(oauth_settings(mcp_bearer_token=BEARER_TOKEN)) == 0
|
||||
|
||||
assert "per-user OAuth is not available" in capsys.readouterr().err
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["*,10.0.0.1", "not-an-ip", "10.0.0.1/99"])
|
||||
def test_invalid_forwarded_allow_ips_is_rejected(value: str) -> None:
|
||||
with pytest.raises(ValueError, match="FORWARDED_ALLOW_IPS"):
|
||||
server_settings(forwarded_allow_ips=value)
|
||||
|
||||
|
||||
def test_blank_forwarded_allow_ips_means_the_default() -> None:
|
||||
assert server_settings(forwarded_allow_ips=" ").forwarded_allow_ips == "127.0.0.1"
|
||||
|
||||
|
||||
# Audit log at startup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unwritable_audit(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
"""An audit path whose parent is a regular file, so it can never be opened."""
|
||||
blocker = tmp_path / "blocker"
|
||||
blocker.write_text("x", encoding="utf-8")
|
||||
path = blocker / "audit.log"
|
||||
monkeypatch.setenv("AUDIT_LOG_PATH", str(path))
|
||||
return path
|
||||
|
||||
|
||||
def test_http_exits_2_when_the_audit_log_cannot_be_opened(
|
||||
uvicorn_spy: UvicornSpy, unwritable_audit: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
assert run_http(server_settings(audit_log_path=unwritable_audit)) == 2
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert uvicorn_spy.calls == []
|
||||
assert "audit log" in captured.err and "AUDIT_LOG_PATH" in captured.err
|
||||
assert str(unwritable_audit) in captured.err
|
||||
assert captured.out == ""
|
||||
|
||||
|
||||
def test_stdio_exits_2_when_the_audit_log_cannot_be_opened(
|
||||
unwritable_audit: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
monkeypatch.setattr(transports.anyio, "run", lambda *a: pytest.fail("must not serve"))
|
||||
|
||||
assert run_stdio(server_settings(audit_log_path=unwritable_audit)) == 2
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "AUDIT_LOG_PATH" in captured.err
|
||||
assert captured.out == ""
|
||||
|
||||
|
||||
def test_oauth_misconfiguration_lists_every_missing_setting(
|
||||
|
||||
Reference in new issue
Block a user