Маршруты /docs и /openapi.json проверяли только адрес. С выключенным списком адресов это означало открытый доступ к описанию API из интернета. Теперь они отвечают 404, пока docs не включён явно, а включённые требуют адрес из списка и токен - заголовком либо ссылкой /docs?token=ЗНАЧЕНИЕ. Попутно исправлено сравнение токена: compare_digest на строках с не-ASCII бросает TypeError, и токен с кириллицей давал 500 вместо 401. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
"""Тесты доступа: токен и список разрешённых адресов."""
|
|
import pytest
|
|
|
|
from app.security import token_matches, check_token, ip_allowed, parse_allowlist
|
|
|
|
|
|
class TestToken:
|
|
def test_correct_token_passes(self):
|
|
assert check_token("Bearer secret123", "secret123") is True
|
|
|
|
def test_wrong_token_fails(self):
|
|
assert check_token("Bearer wrong", "secret123") is False
|
|
|
|
def test_missing_header_fails(self):
|
|
assert check_token(None, "secret123") is False
|
|
|
|
def test_token_without_bearer_prefix_fails(self):
|
|
assert check_token("secret123", "secret123") is False
|
|
|
|
def test_empty_configured_token_denies_everything(self):
|
|
"""Пустой токен в конфиге не должен открывать сервис всем подряд."""
|
|
assert check_token("Bearer ", "") is False
|
|
assert check_token(None, "") is False
|
|
|
|
def test_case_insensitive_scheme(self):
|
|
assert check_token("bearer secret123", "secret123") is True
|
|
|
|
|
|
class TestAllowlist:
|
|
def test_parses_plain_addresses(self):
|
|
nets = parse_allowlist("10.0.0.1, 192.168.1.5")
|
|
assert ip_allowed("10.0.0.1", nets) is True
|
|
assert ip_allowed("10.0.0.2", nets) is False
|
|
|
|
def test_parses_cidr(self):
|
|
nets = parse_allowlist("192.168.1.0/24")
|
|
assert ip_allowed("192.168.1.77", nets) is True
|
|
assert ip_allowed("192.168.2.77", nets) is False
|
|
|
|
def test_empty_allowlist_permits_all(self):
|
|
"""Пустой список означает «ограничение выключено»."""
|
|
nets = parse_allowlist("")
|
|
assert ip_allowed("8.8.8.8", nets) is True
|
|
|
|
def test_ignores_blank_entries(self):
|
|
nets = parse_allowlist("10.0.0.1,, ,10.0.0.2")
|
|
assert ip_allowed("10.0.0.2", nets) is True
|
|
|
|
def test_localhost_v4_and_v6(self):
|
|
nets = parse_allowlist("127.0.0.1, ::1")
|
|
assert ip_allowed("127.0.0.1", nets) is True
|
|
assert ip_allowed("::1", nets) is True
|
|
|
|
def test_malformed_entry_is_skipped_not_crashing(self):
|
|
nets = parse_allowlist("не-адрес, 10.0.0.1")
|
|
assert ip_allowed("10.0.0.1", nets) is True
|
|
|
|
def test_malformed_client_ip_denied(self):
|
|
nets = parse_allowlist("10.0.0.1")
|
|
assert ip_allowed("мусор", nets) is False
|
|
|
|
def test_unknown_client_ip_denied_when_list_set(self):
|
|
nets = parse_allowlist("10.0.0.1")
|
|
assert ip_allowed(None, nets) is False
|
|
|
|
|
|
class TestNonAsciiToken:
|
|
"""compare_digest на строках с не-ASCII бросает TypeError: был бы 500 вместо 401."""
|
|
|
|
def test_cyrillic_token_matches_itself(self):
|
|
assert token_matches("секрет-ключ", "секрет-ключ") is True
|
|
|
|
def test_cyrillic_token_rejects_other(self):
|
|
assert token_matches("другой", "секрет-ключ") is False
|
|
|
|
def test_cyrillic_token_via_header(self):
|
|
assert check_token("Bearer секрет-ключ", "секрет-ключ") is True
|
|
assert check_token("Bearer чужой", "секрет-ключ") is False
|