Dedupe auth.unauthorized floods from unauthenticated page loads.

Keep one 401 audit per IP+path window, and hide historical unauthorized noise from the default business view.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-01 21:48:18 +08:00
parent ea3d45ddc2
commit d35d3992c2
5 changed files with 82 additions and 10 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import logging import logging
import queue import queue
import threading import threading
import time
from typing import Any from typing import Any
from .config import settings from .config import settings
@ -18,6 +19,13 @@ _lock = threading.Lock()
_counter = 0 _counter = 0
_dropped = 0 _dropped = 0
# Dedupe unauthenticated request floods (SPA fires many parallel 401s).
_unauth_lock = threading.Lock()
_unauth_recent: dict[str, float] = {}
_UNAUTH_DEDUP_GET_SEC = 60.0
_UNAUTH_DEDUP_WRITE_SEC = 15.0
_UNAUTH_RECENT_MAX = 4000
# Middleware-tagged HTTP wrappers that duplicate semantic business audits. # Middleware-tagged HTTP wrappers that duplicate semantic business audits.
_MIDDLEWARE_NOISE_ACTIONS = frozenset( _MIDDLEWARE_NOISE_ACTIONS = frozenset(
{ {
@ -61,6 +69,32 @@ def _sample_ok() -> bool:
return (_counter % n) == 0 return (_counter % n) == 0
def should_audit_unauthorized(*, client_ip: str, method: str, path: str) -> bool:
"""Rate-limit auth.unauthorized writes: one row per IP+method+path per window.
Unauthenticated page loads often fan out dozens of GETs in the same second;
keeping every 401 drowns real login/security events.
"""
method_u = str(method or "GET").upper()
window = (
_UNAUTH_DEDUP_WRITE_SEC
if method_u in ("POST", "PUT", "PATCH", "DELETE")
else _UNAUTH_DEDUP_GET_SEC
)
key = f"{client_ip or '-'}|{method_u}|{path or '/'}"
now = time.monotonic()
with _unauth_lock:
global _unauth_recent
last = float(_unauth_recent.get(key) or 0.0)
if now - last < window:
return False
_unauth_recent[key] = now
if len(_unauth_recent) > _UNAUTH_RECENT_MAX:
cutoff = now - max(_UNAUTH_DEDUP_GET_SEC * 5, 300.0)
_unauth_recent = {k: v for k, v in _unauth_recent.items() if float(v) >= cutoff}
return True
def audit_should_persist( def audit_should_persist(
*, *,
action: str, action: str,

View file

@ -113,16 +113,20 @@ class AuthAuditMiddleware(BaseHTTPMiddleware):
try: try:
resolved = resolve_user_from_token(db, token) if token else None resolved = resolve_user_from_token(db, token) if token else None
if resolved is None: if resolved is None:
write_audit( client_ip = _client_ip(request)
db, from .audit_async import should_audit_unauthorized
action="auth.unauthorized",
method=request.method, if should_audit_unauthorized(client_ip=client_ip, method=request.method, path=path):
path=path, write_audit(
status_code=401, db,
client_ip=_client_ip(request), action="auth.unauthorized",
user_agent=str(request.headers.get("user-agent") or "")[:512], method=request.method,
detail={}, path=path,
) status_code=401,
client_ip=client_ip,
user_agent=str(request.headers.get("user-agent") or "")[:512],
detail={},
)
return JSONResponse(status_code=401, content={"detail": "unauthorized"}) return JSONResponse(status_code=401, content={"detail": "unauthorized"})
user, via, scopes, token_id, jti = resolved user, via, scopes, token_id, jti = resolved
if bool(getattr(user, "must_change_password", False)): if bool(getattr(user, "must_change_password", False)):

View file

@ -747,6 +747,7 @@ def list_audit_logs(
"api_tokens.get", "api_tokens.get",
"auth.me", "auth.me",
"auth.sessions", "auth.sessions",
"auth.unauthorized",
) )
q = q.filter(~AuditLog.action.like("http.%")) q = q.filter(~AuditLog.action.like("http.%"))
q = q.filter(~AuditLog.action.in_(noise_actions)) q = q.filter(~AuditLog.action.in_(noise_actions))

View file

@ -60,5 +60,33 @@ class AuditShouldPersistTests(unittest.TestCase):
self.assertFalse(audit_should_persist(action="ume.token.get", method="GET", status_code=200)) self.assertFalse(audit_should_persist(action="ume.token.get", method="GET", status_code=200))
class UnauthorizedDedupeTests(unittest.TestCase):
def setUp(self) -> None:
from netx_api import audit_async as aa
with aa._unauth_lock:
aa._unauth_recent.clear()
def test_dedupes_same_ip_path(self) -> None:
from netx_api.audit_async import should_audit_unauthorized
self.assertTrue(
should_audit_unauthorized(client_ip="127.0.0.1", method="GET", path="/v1/topology")
)
self.assertFalse(
should_audit_unauthorized(client_ip="127.0.0.1", method="GET", path="/v1/topology")
)
# Different path still recorded once.
self.assertTrue(
should_audit_unauthorized(client_ip="127.0.0.1", method="GET", path="/v1/managed-ne")
)
def test_different_ip_not_deduped(self) -> None:
from netx_api.audit_async import should_audit_unauthorized
self.assertTrue(should_audit_unauthorized(client_ip="1.1.1.1", method="GET", path="/v1/x"))
self.assertTrue(should_audit_unauthorized(client_ip="2.2.2.2", method="GET", path="/v1/x"))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -87,6 +87,11 @@ export function auditSummary(
}); });
} }
if (action.startsWith("auth.")) { if (action.startsWith("auth.")) {
if (action === "auth.unauthorized") {
const method = row?.method || "GET";
const path = row?.path || "";
return path ? `${method} ${path}` : "unauthorized";
}
return action.replace(/^auth\./, ""); return action.replace(/^auth\./, "");
} }
if (action.startsWith("http.") || action.startsWith("ume.")) { if (action.startsWith("http.") || action.startsWith("ume.")) {