hermes-agent/tests/gateway/test_multiplex_profile_authz.py
SahilRakhaiya05 bb304b4914 fix(gateway): fail-closed external-surface defaults + profile-aware multiplex authz
Aligns runtime behaviour with SECURITY.md 2.6: externally reachable
messaging adapters must fail closed unless access is explicitly
configured. Closes the confirmed multiplex authorization bypass a
secondary profile's open dm/group policy no longer inherits the default
profile's allowlist trust.

- Own-policy adapters (WhatsApp, WeCom, Weixin, QQBot, Yuanbao) default
  dm_policy/group_policy to pairing/allowlist instead of open; open now
  requires an explicit GATEWAY_ALLOW_ALL_USERS or per-platform allow-all.
- Startup guard (_own_policy_open_startup_violation) refuses to boot when
  an enabled adapter is open without the allow-all opt-in; the guard now
  runs for every secondary profile in multiplex mode too.
- Profile-aware own-policy authorization: _authorization_adapter /
  _adapter_for_source resolve the live adapter via SessionSource.profile,
  so _is_user_authorized and the ingress/pairing/busy/queue paths read the
  originating profile's adapter policy, not the default profile's.
- Fail-closed intake for Email, Feishu P2P, and Discord (blank-principal
  denial, empty-allowlist deny, missing-interaction.user deny).

Salvaged from #44073 (external-surface hardening), split into a focused
gateway-authz PR per maintainer request. Follow-up fix by Hermes Agent:
the Discord slash-auth channel bypass now matches DISCORD_ALLOWED_CHANNELS
by the same name-inclusive keys (id + name + #name + parent) the on_message
scope gate uses, so a name-form channel allowlist authorizes slash
interactions consistently (was id-only, breaking #name matching).

Co-authored-by: Hermes Agent <agent@nousresearch.com>
2026-07-01 03:56:28 -07:00

159 lines
No EOL
5 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_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