From e5580f43c258552b5951b2680a1ac044bab3e5df Mon Sep 17 00:00:00 2001 From: Joel Chan Date: Thu, 28 May 2026 23:45:38 +0800 Subject: [PATCH] fix(discord): propagate role_authorized flag so DISCORD_ALLOWED_ROLES works end-to-end DISCORD_ALLOWED_ROLES was checked by the Discord adapter (_is_allowed_user) but gateway._is_user_authorized only read DISCORD_ALLOWED_USERS, so role-authorized users were rejected with "Unauthorized user" at the gateway layer despite passing the adapter gate. - Add role_authorized: bool = False to SessionSource - Add role_authorized param to build_source (base.py) - Compute _role_authorized in on_message when user passes via role not user ID - Thread _role_authorized through _handle_message -> build_source - Check source.role_authorized early in _is_user_authorized (run.py) Fixes #33952 --- gateway/authz_mixin.py | 5 +++++ gateway/platforms/base.py | 2 ++ gateway/session.py | 1 + plugins/platforms/discord/adapter.py | 7 +++++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/gateway/authz_mixin.py b/gateway/authz_mixin.py index b98118eb5d6..ad3aa854982 100644 --- a/gateway/authz_mixin.py +++ b/gateway/authz_mixin.py @@ -207,6 +207,11 @@ class GatewayAuthorizationMixin: if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in {"true", "1", "yes"}: return True + # Adapter-verified role auth: the Discord adapter already confirmed the + # user holds a role in DISCORD_ALLOWED_ROLES before dispatching the message. + if getattr(source, "role_authorized", False): + return True + if getattr(source, "is_bot", False): allow_bots_var = platform_allow_bots_map.get(source.platform) if allow_bots_var and os.getenv(allow_bots_var, "none").lower().strip() in {"mentions", "all"}: diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 50ca91df263..5e00c9f1ddd 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -4651,6 +4651,7 @@ class BasePlatformAdapter(ABC): guild_id: Optional[str] = None, parent_chat_id: Optional[str] = None, message_id: Optional[str] = None, + role_authorized: bool = False, ) -> SessionSource: """Helper to build a SessionSource for this platform.""" # Normalize empty topic to None @@ -4671,6 +4672,7 @@ class BasePlatformAdapter(ABC): guild_id=str(guild_id) if guild_id else None, parent_chat_id=str(parent_chat_id) if parent_chat_id else None, message_id=str(message_id) if message_id else None, + role_authorized=role_authorized, ) @abstractmethod diff --git a/gateway/session.py b/gateway/session.py index 4d1d26b6467..19aa0cdb776 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -91,6 +91,7 @@ class SessionSource: guild_id: Optional[str] = None # Discord guild / Slack workspace / Matrix server scope parent_chat_id: Optional[str] = None # Parent channel when chat_id refers to a thread message_id: Optional[str] = None # ID of the triggering message (for pin/reply/react) + role_authorized: bool = False # True when adapter granted access via role (not user ID) @property def description(self) -> str: diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index c0dd7221718..d9db208fc4f 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -794,6 +794,7 @@ class DiscordAdapter(BasePlatformAdapter): # Must run BEFORE the user allowlist check so that bots # permitted by DISCORD_ALLOW_BOTS are not rejected for # not being in DISCORD_ALLOWED_USERS (fixes #4466). + _role_authorized = False if getattr(message.author, "bot", False): allow_bots = os.getenv("DISCORD_ALLOW_BOTS", "none").lower().strip() if allow_bots == "none": @@ -817,6 +818,7 @@ class DiscordAdapter(BasePlatformAdapter): is_dm=_is_dm, ): return + _role_authorized = bool(getattr(self, "_allowed_role_ids", set())) # Multi-agent filtering: if the message mentions specific bots # but NOT this bot, the sender is talking to another agent — @@ -858,7 +860,7 @@ class DiscordAdapter(BasePlatformAdapter): if "*" not in _free_channels and not (_channel_ids & _free_channels): return - await self._handle_message(message) + await self._handle_message(message, role_authorized=_role_authorized) @self._client.event async def on_voice_state_update(member, before, after): @@ -4726,7 +4728,7 @@ class DiscordAdapter(BasePlatformAdapter): raise Exception(f"HTTP {resp.status}") return await resp.read() - async def _handle_message(self, message: DiscordMessage) -> None: + async def _handle_message(self, message: DiscordMessage, role_authorized: bool = False) -> None: """Handle incoming Discord messages.""" # In server channels (not DMs), require the bot to be @mentioned # UNLESS the channel is in the free-response list or the message is @@ -4910,6 +4912,7 @@ class DiscordAdapter(BasePlatformAdapter): guild_id=str(guild.id) if guild else None, parent_chat_id=parent_channel_id, message_id=str(message.id), + role_authorized=role_authorized, ) # Build media URLs -- download image attachments to local cache so the