From fef0b2d6004a6fd5a13766725aa2cb16a7c83c12 Mon Sep 17 00:00:00 2001 From: rlaehddus302 Date: Thu, 16 Jul 2026 06:36:02 -0700 Subject: [PATCH] fix(gateway): scope secondary-adapter auth callback to its own profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- gateway/run.py | 10 +++- tests/gateway/test_multiplex_profile_authz.py | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/gateway/run.py b/gateway/run.py index 751a9ace8..430704fbe 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -8875,7 +8875,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew adapter.set_session_store(self.session_store) adapter.set_busy_session_handler(self._handle_active_session_busy_message) adapter.set_topic_recovery_fn(self._recover_telegram_topic_thread_id) - adapter.set_authorization_check(self._make_adapter_auth_check(adapter.platform)) + adapter.set_authorization_check( + self._make_adapter_auth_check(adapter.platform, profile_name=profile_name) + ) adapter._busy_text_mode = self._busy_text_mode try: @@ -9092,6 +9094,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew def _make_adapter_auth_check( self, platform: Platform, + profile_name: Optional[str] = None, ) -> Callable[[str, Optional[str], Optional[str]], bool]: """Build a platform-bound auth callback for adapter use. @@ -9104,6 +9107,10 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew The returned callback delegates to :meth:`_is_user_authorized` so the full auth chain — platform allowlists, group allowlists, pairing store, allow-all flags — stays the single source of truth. + + ``profile_name`` binds the callback to the secondary adapter's own + multiplex profile, so its ``SessionSource`` resolves that profile's + secret scope instead of falling back to the active profile. """ def check( user_id: str, @@ -9117,6 +9124,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew chat_id=chat_id or "", chat_type=chat_type or "group", user_id=user_id, + profile=profile_name, ) return self._is_user_authorized(source) return check diff --git a/tests/gateway/test_multiplex_profile_authz.py b/tests/gateway/test_multiplex_profile_authz.py index 514bda7cc..fca5060b5 100644 --- a/tests/gateway/test_multiplex_profile_authz.py +++ b/tests/gateway/test_multiplex_profile_authz.py @@ -139,6 +139,56 @@ def test_secondary_allowlist_dm_behavior_ignores_unauthorized(monkeypatch): 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