Files
aiturk-hermes-ide/tests/gateway/test_buzz_authz.py
T

185 lines
7.0 KiB
Python

"""Gateway authz tests: BUZZ_ALLOWED_USERS accepts npub or hex (#78428).
Inbound Buzz events carry the sender as a 64-char hex pubkey, while
``BUZZ_ALLOWED_USERS`` historically accepted npubs too. The gateway's
central allowlist comparison must decode npub entries to hex at comparison
time, or an operator who listed only their npub is rejected with
"Unauthorized user: <hex pubkey>" (gateway drops the message).
"""
import pytest
from gateway.config import Platform
from gateway.platform_registry import PlatformEntry, platform_registry
from gateway.session import SessionSource
# Chip's public identity (public information, not a secret) — the same pair
# used by tests/gateway/test_buzz_adapter.py.
SELF_PUBKEY = "9fd5c7ba6d3ef224da78f541e0fcb9c50f72cc63edb19aae76ac6a0474dfa860"
SELF_NPUB = "npub1nl2u0wnd8mezfknc74q7pl9ec58h9nrrakce4tnk434qgaxl4psqe5twr6"
OTHER_PUBKEY = "a" * 64
_BUZZ_PLATFORM = Platform("buzz")
_AUTH_ENV_VARS = (
"BUZZ_ALLOWED_USERS",
"BUZZ_ALLOW_ALL_USERS",
"GATEWAY_ALLOWED_USERS",
"GATEWAY_ALLOW_ALL_USERS",
)
@pytest.fixture(autouse=True)
def _isolate_env(monkeypatch):
for var in _AUTH_ENV_VARS:
monkeypatch.delenv(var, raising=False)
@pytest.fixture
def buzz_registered():
"""Register a minimal Buzz platform entry (allowed_users_env contract)."""
platform_registry.register(
PlatformEntry(
name="buzz",
label="Buzz",
adapter_factory=lambda cfg: None,
check_fn=lambda: True,
allowed_users_env="BUZZ_ALLOWED_USERS",
allow_all_env="BUZZ_ALLOW_ALL_USERS",
)
)
yield
platform_registry.unregister("buzz")
def _make_runner():
from gateway.run import GatewayRunner
runner = object.__new__(GatewayRunner)
runner.pairing_store = None
return runner
def _make_source(user_id: str, chat_type: str = "dm"):
return SessionSource(
platform=_BUZZ_PLATFORM,
chat_id="ccc2bc1a-7a82-5a8f-8c4e-57a070cbe7cd",
chat_type=chat_type,
user_id=user_id,
user_name="Chip",
is_bot=False,
)
def test_npub_only_allowlist_authorizes_hex_identity(monkeypatch, buzz_registered):
"""The reported bug: listing only the npub must authorize the hex pubkey."""
monkeypatch.setenv("BUZZ_ALLOWED_USERS", SELF_NPUB)
runner = _make_runner()
assert runner._is_user_authorized(_make_source(SELF_PUBKEY)) is True
def test_hex_only_allowlist_still_authorizes(monkeypatch, buzz_registered):
"""Existing hex-only allowlists keep working unchanged."""
monkeypatch.setenv("BUZZ_ALLOWED_USERS", SELF_PUBKEY)
runner = _make_runner()
assert runner._is_user_authorized(_make_source(SELF_PUBKEY)) is True
def test_mixed_npub_and_hex_allowlist_authorizes(monkeypatch, buzz_registered):
"""Both forms in one allowlist authorize the same identity."""
monkeypatch.setenv("BUZZ_ALLOWED_USERS", f"{SELF_NPUB},{SELF_PUBKEY}")
runner = _make_runner()
assert runner._is_user_authorized(_make_source(SELF_PUBKEY)) is True
def test_npub_allowlist_still_denies_other_user(monkeypatch, buzz_registered):
"""Normalization must not turn into fail-open for unrelated senders."""
monkeypatch.setenv("BUZZ_ALLOWED_USERS", SELF_NPUB)
runner = _make_runner()
assert runner._is_user_authorized(_make_source(OTHER_PUBKEY)) is False
def test_uppercase_npub_allowlist_authorizes(monkeypatch, buzz_registered):
"""npub entries are case-insensitive, like the adapter's own decoder."""
monkeypatch.setenv("BUZZ_ALLOWED_USERS", SELF_NPUB.upper())
runner = _make_runner()
assert runner._is_user_authorized(_make_source(SELF_PUBKEY)) is True
def test_normalize_nostr_allow_entries():
from gateway.authz_mixin import _normalize_nostr_allow_entries
expanded = _normalize_nostr_allow_entries({SELF_NPUB, OTHER_PUBKEY, "not-a-key"})
assert SELF_PUBKEY in expanded
assert SELF_NPUB in expanded # original kept, harmless
assert OTHER_PUBKEY in expanded
assert "not-a-key" in expanded
def test_npub_to_hex_roundtrip():
from gateway.authz_mixin import _npub_to_hex
assert _npub_to_hex(SELF_NPUB) == SELF_PUBKEY
assert _npub_to_hex(SELF_PUBKEY) is None # hex input is not an npub
assert _npub_to_hex("npub1garbage!!") is None # invalid bech32
assert _npub_to_hex("") is None
# ─────────────────────────────────────────────────────────────────────
# #82871: single-profile gateway must consult the Buzz adapter's own
# config.extra.allowed_users when no env allowlist is configured. The
# reported symptom was a default-deny of EVERY Buzz user ("Unauthorized
# user: <hex> on buzz") even though the sender's npub was correctly
# listed in gateway.platforms.buzz.extra.allowed_users.
# ─────────────────────────────────────────────────────────────────────
def _make_runner_with_adapter(extra: dict):
"""Single-profile (multiplex OFF) runner with a live Buzz adapter."""
from types import SimpleNamespace
from gateway.authz_mixin import _npub_to_hex
from gateway.config import GatewayConfig, PlatformConfig
from gateway.run import GatewayRunner
def _normalize(entry):
entry = (entry or "").strip()
if entry.lower().startswith("npub1"):
return _npub_to_hex(entry)
return entry.lower() if entry else None
runner = object.__new__(GatewayRunner)
runner.config = GatewayConfig()
runner.pairing_store = None
runner.adapters = {
_BUZZ_PLATFORM: SimpleNamespace(
config=PlatformConfig(enabled=True, extra=extra),
normalize_user_id=_normalize,
)
}
return runner
def test_config_only_npub_allowlist_authorizes_single_profile(buzz_registered):
"""#82871 repro: npub listed ONLY in config.extra.allowed_users (no env
var at all) must authorize the sender's hex pubkey."""
runner = _make_runner_with_adapter({"allowed_users": [SELF_NPUB]})
assert runner._is_user_authorized(_make_source(SELF_PUBKEY)) is True
def test_config_only_hex_allowlist_authorizes_single_profile(buzz_registered):
runner = _make_runner_with_adapter({"allowed_users": [SELF_PUBKEY]})
assert runner._is_user_authorized(_make_source(SELF_PUBKEY)) is True
def test_config_only_allowlist_still_denies_unlisted_sender(buzz_registered):
"""Consulting the adapter allowlist must not fail open."""
runner = _make_runner_with_adapter({"allowed_users": [SELF_NPUB]})
assert runner._is_user_authorized(_make_source(OTHER_PUBKEY)) is False
def test_config_only_empty_allowlist_keeps_default_deny(buzz_registered):
"""No allowlist anywhere: the default-deny is preserved (SECURITY.md §2.6)."""
runner = _make_runner_with_adapter({})
assert runner._is_user_authorized(_make_source(SELF_PUBKEY)) is False