fix(rate-limit): secchio login esteso a credenziali supervisore e setup (#8)

Il secchio "login" del rate limit valeva solo per /api/auth/login. Un
capoturno può ora essere attaccato a forza bruta anche via
/api/measurements/{id}/authorise e /api/production-runs/{id}/pause|
resume|close, senza mai toccare il bucket più permissivo "general".
_is_login_bucket_request confronta il path con un'espressione regolare,
non con ==, perché l'id in mezzo varia per richiesta.

Include anche la parte di #9 che riguarda questo stesso file: i path
sotto /api/setup/ entrano nello stesso secchio, indipendentemente dal
metodo (GET /api/setup/status accetta ora una password opzionale).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:34:42 +02:00
parent 1921bc3302
commit e91ef739af
3 changed files with 100 additions and 4 deletions
@@ -121,6 +121,44 @@ def test_trusted_proxy_count_zero_ignores_all_proxy_headers(monkeypatch):
assert RateLimitMiddleware._client_ip(req) == "172.18.0.9"
class TestLoginBucketPaths:
"""_is_login_bucket_request: which paths share the login rate-limit bucket."""
def test_login_path_is_login_bucket(self):
assert RateLimitMiddleware._is_login_bucket_request("POST", "/api/auth/login")
def test_login_path_get_is_not_login_bucket(self):
assert not RateLimitMiddleware._is_login_bucket_request("GET", "/api/auth/login")
def test_measurement_authorise_is_login_bucket(self):
assert RateLimitMiddleware._is_login_bucket_request(
"POST", "/api/measurements/42/authorise"
)
def test_production_pause_resume_close_are_login_bucket(self):
for action in ("pause", "resume", "close"):
assert RateLimitMiddleware._is_login_bucket_request(
"POST", f"/api/production-runs/7/{action}"
)
def test_production_cycle_is_not_login_bucket(self):
"""Only the supervisor-gated actions, not every production-runs path."""
assert not RateLimitMiddleware._is_login_bucket_request(
"POST", "/api/production-runs/7/cycle"
)
def test_setup_paths_are_login_bucket_any_method(self):
assert RateLimitMiddleware._is_login_bucket_request("POST", "/api/setup/init-db")
assert RateLimitMiddleware._is_login_bucket_request("GET", "/api/setup/status")
def test_bare_setup_page_is_not_login_bucket(self):
"""The HTML setup page itself carries no password."""
assert not RateLimitMiddleware._is_login_bucket_request("GET", "/api/setup")
def test_unrelated_path_is_not_login_bucket(self):
assert not RateLimitMiddleware._is_login_bucket_request("POST", "/api/recipes")
class TestEffectiveLimit:
"""_effective_limit: dividing a configured limit across worker processes."""