From ca8aee87e8162d7fe2c757c08f164d6cabe59a2b Mon Sep 17 00:00:00 2001 From: benjamin2026-dot Date: Thu, 23 Jul 2026 08:17:07 -0700 Subject: [PATCH] fix(slack): route file downloads to the owning workspace via URL-embedded team id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salvaged from #59742 (downloads half only). Main already resolves the event-level team id from the channel→workspace cache before downloads (the file-EVENT half was covered by #30456), but when neither the event nor the cache knows the workspace, both download helpers silently fell back to the PRIMARY workspace token — Slack then returns an HTML login page instead of file bytes for any non-primary workspace file. Slack private file URLs embed the owning workspace id (files-pri/-/...), so _resolve_download_token now prefers: explicit team_id -> URL-embedded team id -> primary token. Both _download_slack_file and _download_slack_file_bytes route through it. Deferred from #59742 (out of this correctness cluster's scope): the channel→team disk persistence, the pre-send conversations.info probe loop, and the thread-file ingestion feature. Reapplied from #59742 by @benjamin2026-dot onto the current adapter (original targeted the pre-#30456 download sites). --- plugins/platforms/slack/adapter.py | 37 ++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 8004b0689712..437af04291cd 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -7879,6 +7879,31 @@ class SlackAdapter(BasePlatformAdapter): cls._SLACK_CDN_HOST_SUFFIXES ) + def _resolve_download_token(self, url: str, team_id: str = "") -> str: + """Pick the correct bot token for a Slack file download. + + Order of preference: + 1. Explicit team_id that maps to a known workspace client. + 2. team_id parsed from the file URL itself — Slack private file URLs + embed the workspace id as ``files-pri/-/...`` so + we can route to the right workspace even when the triggering event + carried no team info (thread replies / mentions in multi-workspace + installs). This prevents defaulting to the primary workspace token, + which makes Slack return an HTML login page instead of file bytes. + 3. Primary workspace token as a last resort. + """ + if team_id and team_id in self._team_clients: + return self._team_clients[team_id].token + try: + m = re.search(r"/files-pri/(T[A-Z0-9]+)-", url or "") + if m: + url_team = m.group(1) + if url_team in self._team_clients: + return self._team_clients[url_team].token + except Exception: # pragma: no cover - defensive + pass + return self.config.token or "" + async def _download_slack_file( self, url: str, ext: str, audio: bool = False, team_id: str = "" ) -> str: @@ -7909,11 +7934,7 @@ class SlackAdapter(BasePlatformAdapter): f"{safe_url_for_log(url)}" ) - bot_token = ( - self._team_clients[team_id].token - if team_id and team_id in self._team_clients - else self.config.token - ) + bot_token = self._resolve_download_token(url, team_id) # DNS-pinned client: resolve + validate once, dial the vetted IP # (closes the DNS-rebinding TOCTOU window between is_safe_url and @@ -7989,11 +8010,7 @@ class SlackAdapter(BasePlatformAdapter): f"{safe_url_for_log(url)}" ) - bot_token = ( - self._team_clients[team_id].token - if team_id and team_id in self._team_clients - else self.config.token - ) + bot_token = self._resolve_download_token(url, team_id) # DNS-pinned client: resolve + validate once, dial the vetted IP # (closes the DNS-rebinding TOCTOU window between is_safe_url and