mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
Subset of PR #61985: _make_adapter_auth_check gains a profile_name parameter and secondary-profile adapters (started in _start_one_profile_adapters) bind it, so the auth callback's SessionSource resolves the routed profile's adapter and pairing store instead of silently falling back to the default profile. This is the gap left open by the #65629 merge — adapter-internal auth checks (e.g. Slack thread-context fetch) fire outside the wrapped message handler. The PR's authz_mixin.py hunks are dropped: main's _auth_env (merged via PR #65629) already covers the scoped allowlist reads they targeted.
209 lines
No EOL
6.6 KiB
Python
209 lines
No EOL
6.6 KiB
Python
"""Regression tests for multiplex profile-aware own-policy authorization."""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from gateway.config import GatewayConfig, Platform, PlatformConfig
|
|
from gateway.session import SessionSource
|
|
|
|
|
|
def _clear_auth_env(monkeypatch) -> None:
|
|
for key in (
|
|
"WECOM_ALLOWED_USERS",
|
|
"GATEWAY_ALLOWED_USERS",
|
|
"GATEWAY_ALLOW_ALL_USERS",
|
|
"WECOM_ALLOW_ALL_USERS",
|
|
):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
|
|
def _make_multiplex_runner(monkeypatch):
|
|
"""Runner with default allowlist WeCom and secondary open-policy WeCom."""
|
|
from gateway.run import GatewayRunner
|
|
|
|
_clear_auth_env(monkeypatch)
|
|
|
|
runner = object.__new__(GatewayRunner)
|
|
runner.config = GatewayConfig(multiplex_profiles=True)
|
|
|
|
default_adapter = SimpleNamespace(
|
|
send=AsyncMock(),
|
|
enforces_own_access_policy=True,
|
|
_dm_policy="allowlist",
|
|
_group_policy="pairing",
|
|
)
|
|
secondary_adapter = SimpleNamespace(
|
|
send=AsyncMock(),
|
|
enforces_own_access_policy=True,
|
|
_dm_policy="open",
|
|
_group_policy="open",
|
|
)
|
|
|
|
runner.adapters = {Platform.WECOM: default_adapter}
|
|
runner._profile_adapters = {
|
|
"coder": {Platform.WECOM: secondary_adapter},
|
|
}
|
|
runner.pairing_store = MagicMock()
|
|
runner.pairing_store.is_approved.return_value = False
|
|
return runner, default_adapter, secondary_adapter
|
|
|
|
|
|
def test_secondary_open_policy_not_authorized_by_default_allowlist(monkeypatch):
|
|
"""Secondary-profile open intake must not inherit default allowlist trust."""
|
|
runner, _default_adapter, _secondary_adapter = _make_multiplex_runner(monkeypatch)
|
|
|
|
source = SessionSource(
|
|
platform=Platform.WECOM,
|
|
user_id="attacker",
|
|
chat_id="dm-chat",
|
|
user_name="attacker",
|
|
chat_type="dm",
|
|
profile="coder",
|
|
)
|
|
|
|
assert runner._adapter_dm_policy(Platform.WECOM, profile="coder") == "open"
|
|
assert runner._adapter_dm_policy(Platform.WECOM) == "allowlist"
|
|
assert runner._is_user_authorized(source) is False
|
|
|
|
|
|
def test_default_profile_still_trusts_own_allowlist(monkeypatch):
|
|
"""Default-profile allowlist trust is unchanged when profile is unstamped."""
|
|
runner, _default_adapter, _secondary_adapter = _make_multiplex_runner(monkeypatch)
|
|
|
|
source = SessionSource(
|
|
platform=Platform.WECOM,
|
|
user_id="allowed-user",
|
|
chat_id="dm-chat",
|
|
user_name="allowed-user",
|
|
chat_type="dm",
|
|
profile=None,
|
|
)
|
|
|
|
assert runner._is_user_authorized(source) is True
|
|
|
|
|
|
def test_secondary_allowlist_still_authorized(monkeypatch):
|
|
"""Secondary profile with allowlist policy is trusted on its own adapter."""
|
|
runner, _default_adapter, secondary_adapter = _make_multiplex_runner(monkeypatch)
|
|
secondary_adapter._dm_policy = "allowlist"
|
|
|
|
source = SessionSource(
|
|
platform=Platform.WECOM,
|
|
user_id="allowed-user",
|
|
chat_id="dm-chat",
|
|
user_name="allowed-user",
|
|
chat_type="dm",
|
|
profile="coder",
|
|
)
|
|
|
|
assert runner._is_user_authorized(source) is True
|
|
|
|
|
|
def test_adapter_for_source_resolves_secondary_profile_adapter(monkeypatch):
|
|
"""Ingress adapter lookup must use the stamped profile's adapter map."""
|
|
runner, default_adapter, secondary_adapter = _make_multiplex_runner(monkeypatch)
|
|
|
|
source = SessionSource(
|
|
platform=Platform.WECOM,
|
|
user_id="attacker",
|
|
chat_id="dm-chat",
|
|
user_name="attacker",
|
|
chat_type="dm",
|
|
profile="coder",
|
|
)
|
|
|
|
assert runner._adapter_for_source(source) is secondary_adapter
|
|
assert runner._adapter_for_source(
|
|
SessionSource(
|
|
platform=Platform.WECOM,
|
|
user_id="allowed-user",
|
|
chat_id="dm-chat",
|
|
user_name="allowed-user",
|
|
chat_type="dm",
|
|
profile=None,
|
|
)
|
|
) is default_adapter
|
|
|
|
|
|
def test_secondary_allowlist_dm_behavior_ignores_unauthorized(monkeypatch):
|
|
"""Unauthorized-DM behavior must read the secondary adapter's dm_policy."""
|
|
runner, _default_adapter, secondary_adapter = _make_multiplex_runner(monkeypatch)
|
|
secondary_adapter._dm_policy = "allowlist"
|
|
|
|
assert runner._get_unauthorized_dm_behavior(
|
|
Platform.WECOM,
|
|
profile="coder",
|
|
) == "ignore"
|
|
assert runner._get_unauthorized_dm_behavior(Platform.WECOM) == "ignore"
|
|
|
|
|
|
def test_adapter_auth_check_stamps_secondary_profile(monkeypatch):
|
|
"""The adapter auth-check callback must stamp its own secondary profile.
|
|
|
|
Regression for the gap where ``_make_adapter_auth_check`` built a
|
|
profile-less ``SessionSource``, so a secondary adapter's external-context
|
|
authorization (e.g. Slack/Discord thread-reply lookups) silently
|
|
resolved the *active* profile's allowlist scope instead of its own.
|
|
"""
|
|
from gateway.run import GatewayRunner
|
|
|
|
_clear_auth_env(monkeypatch)
|
|
|
|
runner = object.__new__(GatewayRunner)
|
|
runner.config = GatewayConfig(multiplex_profiles=True)
|
|
|
|
captured: dict = {}
|
|
|
|
def fake_is_user_authorized(source):
|
|
captured["profile"] = source.profile
|
|
return True
|
|
|
|
runner._is_user_authorized = fake_is_user_authorized
|
|
|
|
check = runner._make_adapter_auth_check(Platform.WECOM, profile_name="coder")
|
|
assert check("some-user", "dm", "dm-chat") is True
|
|
assert captured["profile"] == "coder"
|
|
|
|
|
|
def test_adapter_auth_check_defaults_to_active_profile(monkeypatch):
|
|
"""Primary-adapter callbacks (no profile_name) still resolve the active profile."""
|
|
from gateway.run import GatewayRunner
|
|
|
|
_clear_auth_env(monkeypatch)
|
|
|
|
runner = object.__new__(GatewayRunner)
|
|
runner.config = GatewayConfig(multiplex_profiles=True)
|
|
|
|
captured: dict = {}
|
|
|
|
def fake_is_user_authorized(source):
|
|
captured["profile"] = source.profile
|
|
return True
|
|
|
|
runner._is_user_authorized = fake_is_user_authorized
|
|
|
|
check = runner._make_adapter_auth_check(Platform.WECOM)
|
|
assert check("some-user", "dm", "dm-chat") is True
|
|
assert captured["profile"] is None
|
|
|
|
|
|
def test_secondary_open_policy_fails_startup_guard(monkeypatch):
|
|
"""Secondary profiles must pass the same open-policy startup guard."""
|
|
from gateway.run import _own_policy_open_startup_violation
|
|
|
|
_clear_auth_env(monkeypatch)
|
|
|
|
secondary_cfg = GatewayConfig(multiplex_profiles=True)
|
|
secondary_cfg.platforms = {
|
|
Platform.WECOM: PlatformConfig(
|
|
enabled=True,
|
|
extra={"dm_policy": "open"},
|
|
),
|
|
}
|
|
|
|
violation = _own_policy_open_startup_violation(secondary_cfg)
|
|
assert violation is not None
|
|
assert "wecom" in violation
|
|
assert "open policy" in violation |