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
+18
-11
@@ -25,6 +25,7 @@ from aegis_gitea_mcp.gitea import RAW_TOOL_NAME
|
||||
from aegis_gitea_mcp.identity import Identity
|
||||
from aegis_gitea_mcp.transports import (
|
||||
GiteaProbe,
|
||||
RateLimitMiddleware,
|
||||
build_http_app,
|
||||
transport_security,
|
||||
)
|
||||
@@ -336,11 +337,7 @@ def test_transport_security_wildcard_bind_adds_no_bind_host() -> None:
|
||||
# Rate limiting
|
||||
|
||||
|
||||
def test_rate_limit_returns_429_on_the_mcp_path_only(
|
||||
httpx_mock: HTTPXMock, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# The limiter reads the process-wide settings.
|
||||
monkeypatch.setenv("RATE_LIMIT_PER_MINUTE", "2")
|
||||
def test_rate_limit_returns_429_on_the_mcp_path_only(httpx_mock: HTTPXMock) -> None:
|
||||
httpx_mock.add_response(url=f"{API}/version", json={}, is_reusable=True)
|
||||
settings = http_settings(rate_limit_per_minute=2)
|
||||
with TestClient(build_http_app(settings), base_url=BASE) as c:
|
||||
@@ -357,22 +354,32 @@ def test_rate_limit_returns_429_on_the_mcp_path_only(
|
||||
assert health.status_code == 200
|
||||
|
||||
|
||||
def test_rate_limit_is_keyed_per_token(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("RATE_LIMIT_PER_MINUTE", "1")
|
||||
def test_rate_limit_is_keyed_per_token_hash() -> None:
|
||||
settings = http_settings(rate_limit_per_minute=1)
|
||||
with TestClient(build_http_app(settings), base_url=BASE) as c:
|
||||
app = build_http_app(settings)
|
||||
with TestClient(app, base_url=BASE) as c:
|
||||
first = c.post("/mcp", json=INITIALIZE, headers={**MCP_HEADERS, **auth()})
|
||||
limiter = next(m for m in app.user_middleware if m.cls is RateLimitMiddleware).kwargs["limiter"]
|
||||
|
||||
assert first.status_code == 200
|
||||
from aegis_gitea_mcp.rate_limit import get_rate_limiter
|
||||
|
||||
limiter = get_rate_limiter()
|
||||
assert limiter._token_limiter.max_requests == 1
|
||||
# The raw header is never used as a key, only its hash.
|
||||
keys = list(limiter._token_limiter._events)
|
||||
assert keys and all(BEARER_TOKEN not in key for key in keys)
|
||||
|
||||
|
||||
def test_rate_limit_uses_the_settings_passed_to_the_app(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("RATE_LIMIT_PER_MINUTE", "500") # the process-wide value must not matter
|
||||
app = build_http_app(http_settings(rate_limit_per_minute=1))
|
||||
with TestClient(app, base_url=BASE) as c:
|
||||
statuses = [
|
||||
c.post("/mcp", json=INITIALIZE, headers={**MCP_HEADERS, **auth()}).status_code
|
||||
for _ in range(2)
|
||||
]
|
||||
|
||||
assert statuses == [200, 429]
|
||||
|
||||
|
||||
# Auth wiring
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user