Spaces:
Running
Running
sync: 181 file da Baida98/AI@80e22d8c (2026-08-25 12:01 UTC) [deploy-all]
#81
by Baida07 - opened
- api/auth_guard.py +10 -5
- tests/test_security.py +26 -0
api/auth_guard.py
CHANGED
|
@@ -124,15 +124,20 @@ def _prune_expired_rate_keys(now: float, window_s: float) -> None:
|
|
| 124 |
def _rate_key(role: int, token_header: str | None, client_ip: str | None = None) -> str:
|
| 125 |
"""Chiave rate limiter: hash(role + discriminante) — non espone token né IP in chiaro.
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
| 130 |
"""
|
| 131 |
if role == 0:
|
| 132 |
-
# USER:
|
| 133 |
raw = f"0:{client_ip or 'unknown'}"
|
|
|
|
|
|
|
|
|
|
| 134 |
else:
|
| 135 |
-
#
|
| 136 |
raw = f"{role}:{token_header or 'anonymous'}"
|
| 137 |
return _rl_hash.sha256(raw.encode()).hexdigest()[:16]
|
| 138 |
|
|
|
|
| 124 |
def _rate_key(role: int, token_header: str | None, client_ip: str | None = None) -> str:
|
| 125 |
"""Chiave rate limiter: hash(role + discriminante) — non espone token né IP in chiaro.
|
| 126 |
|
| 127 |
+
USER usa sempre l'IP come discriminante. MACHINE usa l'IP quando il proxy
|
| 128 |
+
fidato lo inoltra: Cloudflare usa un unico token interno per tutti i browser,
|
| 129 |
+
quindi il solo token renderebbe globale il limite di 30 richieste/minuto.
|
| 130 |
+
In assenza di IP attestato, MACHINE conserva il fallback per-token. OPERATOR
|
| 131 |
+
e ADMIN mantengono il bucket per-token.
|
| 132 |
"""
|
| 133 |
if role == 0:
|
| 134 |
+
# USER: bucket per IP, mai globale condiviso.
|
| 135 |
raw = f"0:{client_ip or 'unknown'}"
|
| 136 |
+
elif role == 1 and client_ip:
|
| 137 |
+
# MACHINE via proxy fidato: separa gli utenti dietro INTERNAL_TOKEN.
|
| 138 |
+
raw = f"1:{client_ip}"
|
| 139 |
else:
|
| 140 |
+
# Chiamate server-to-server e ruoli elevati: bucket per token.
|
| 141 |
raw = f"{role}:{token_header or 'anonymous'}"
|
| 142 |
return _rl_hash.sha256(raw.encode()).hexdigest()[:16]
|
| 143 |
|
tests/test_security.py
CHANGED
|
@@ -132,6 +132,32 @@ class TestRequireRole(unittest.TestCase):
|
|
| 132 |
_run(dep(req_mock, resolved=self.AR.USER))
|
| 133 |
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
class TestBrowserEndpointAuth(unittest.TestCase):
|
| 136 |
"""SEC-5: endpoint browser stateless non fail-open (GAP-1-fix)."""
|
| 137 |
|
|
|
|
| 132 |
_run(dep(req_mock, resolved=self.AR.USER))
|
| 133 |
|
| 134 |
|
| 135 |
+
class TestMachineRateLimitClientBuckets(unittest.TestCase):
|
| 136 |
+
"""SEC-3B: il token proxy MACHINE non deve condividere un unico bucket pubblico."""
|
| 137 |
+
|
| 138 |
+
def setUp(self):
|
| 139 |
+
try:
|
| 140 |
+
from api.auth_guard import _rate_key
|
| 141 |
+
self.rate_key = _rate_key
|
| 142 |
+
except ImportError as e:
|
| 143 |
+
self.skipTest(str(e))
|
| 144 |
+
|
| 145 |
+
def test_machine_uses_distinct_attested_client_ips(self):
|
| 146 |
+
first = self.rate_key(1, "shared-internal-token", "198.51.100.10")
|
| 147 |
+
second = self.rate_key(1, "shared-internal-token", "198.51.100.11")
|
| 148 |
+
self.assertNotEqual(first, second)
|
| 149 |
+
|
| 150 |
+
def test_machine_without_attested_ip_keeps_token_bucket(self):
|
| 151 |
+
first = self.rate_key(1, "shared-internal-token", None)
|
| 152 |
+
second = self.rate_key(1, "shared-internal-token", None)
|
| 153 |
+
self.assertEqual(first, second)
|
| 154 |
+
|
| 155 |
+
def test_operator_remains_token_scoped(self):
|
| 156 |
+
first = self.rate_key(2, "operator-token", "198.51.100.10")
|
| 157 |
+
second = self.rate_key(2, "operator-token", "198.51.100.11")
|
| 158 |
+
self.assertEqual(first, second)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
class TestBrowserEndpointAuth(unittest.TestCase):
|
| 162 |
"""SEC-5: endpoint browser stateless non fail-open (GAP-1-fix)."""
|
| 163 |
|