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
This commit is contained in:
Joel Chan 2026-05-28 23:45:38 +08:00 committed by Teknium
parent 5a4297a11a
commit e5580f43c2
4 changed files with 13 additions and 2 deletions

View file

@ -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"}:

View file

@ -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

View file

@ -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:

View file

@ -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