Skip to content

feat(slack): scope private channel visibility to installing user#4779

Merged
waleedlatif1 merged 3 commits into
stagingfrom
waleedlatif1/slack-private-channel-privacy
May 28, 2026
Merged

feat(slack): scope private channel visibility to installing user#4779
waleedlatif1 merged 3 commits into
stagingfrom
waleedlatif1/slack-private-channel-privacy

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Capture the installing user's Slack authed_user.id at OAuth connect time (via better-auth tokens.raw) and tag the credential's accountId with a usr_ marker
  • Scope the channel selector to the credential owner's own Slack membership using users.conversations (bot groups:read) — private channels the owner isn't a member of are hidden, even when the bot has access (Slack Marketplace privacy requirement)
  • Public channels unaffected; legacy credentials and BYO bot tokens fall back to the existing is_member behavior (no regression for existing connections)
  • Fail closed if membership can't be verified; follow cursor pagination so the channel list is complete

Type of Change

  • Bug fix

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 28, 2026 10:42pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 28, 2026

PR Summary

Medium Risk
Changes OAuth credential identity format and channel-selector privacy logic; mis-parsing or API failures could hide private channels or affect re-connect behavior, though legacy paths are preserved.

Overview
Slack channel picker now limits private channels to those the OAuth installer belongs to (Slack Marketplace privacy), instead of any private channel the bot can see.

On new Slack OAuth connects, auth.ts embeds the installing user’s authed_user.id in the credential accountId via a usr_ marker. The channels API reads that id, loads the installer’s private channels with users.conversations, and only shows private channels in that set; if that lookup fails, all private channels are hidden (fail closed). Legacy credentials and direct bot tokens still use the prior bot is_member behavior.

Channel listing now paginates conversations.list / users.conversations (cursor, capped pages) and logs when results may be truncated.

Reviewed by Cursor Bugbot for commit be61b2d. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Greptile Summary

This PR scopes private Slack channel visibility to the installing user's own workspace membership, satisfying Slack Marketplace's privacy requirement that a private channel must only be shown to a user whose Slack account is a member — even when the bot has been invited.

  • auth.ts: At OAuth connect time, authed_user.id is captured from tokens.raw and embedded in accountId behind a usr_ marker (${teamId}-usr_${userId}-${uuid}). Legacy credentials that lack the marker are left unchanged, so existing connections see no behaviour change.
  • route.ts: For new credentials, the accountId is parsed to extract the installing user's Slack ID, which drives a users.conversations?user= call to build the allowed private-channel set. Private channels are shown only when the user is a member; if the membership fetch fails the handler fails closed (hides all private channels). Public channels are unaffected in all code paths.
  • Pagination is now fully followed via a bounded loop (SLACK_MAX_PAGES = 10, 200 channels/page), with a truncated flag surfaced in logs when the cap is hit.

Confidence Score: 5/5

Safe to merge. Both changed files are well-contained, backward-compatible, and the new code paths all have explicit fallbacks.

The auth change is purely additive — new credentials get the usr_ marker, legacy credentials are untouched. The channel-filtering logic is correctly fail-closed (empty set on error), pagination is bounded and the truncated flag is surfaced in logs, and the regex correctly narrowed to [UW] user IDs as addressed in prior review rounds. No data loss, no broken code paths.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/api/tools/slack/channels/route.ts Adds installer-scoped private channel filtering via users.conversations, full cursor pagination with a bounded loop and truncated sentinel, and a DB lookup to extract the scoped user ID from the credential's accountId. Logic is well-structured with correct fail-closed behavior and legacy-credential fallback.
apps/sim/lib/auth/auth.ts Tags new Slack OAuth credentials with a usr_<userId> marker in accountId by reading tokens.raw.authed_user.id. Falls back to the existing user_id / bot_id / 'bot' format when authed_user is absent, preserving full backward compatibility with legacy credentials.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as channels/route.ts
    participant DB as Database
    participant SlackBot as Slack API (bot token)
    participant SlackUser as Slack API (users.conversations)

    Client->>Route: POST /api/tools/slack/channels
    Route->>Route: authorizeCredentialUse()
    Route->>DB: "SELECT accountId WHERE id = resolvedCredentialId"
    DB-->>Route: accountId (e.g. T123-usr_U456-uuid)
    Route->>Route: parseScopedSlackUserId(accountId) → scopedUserId

    Route->>SlackBot: conversations.list (paginated, ≤10 pages)
    SlackBot-->>Route: all bot-accessible channels

    alt scopedUserId present (new credentials)
        Route->>SlackUser: "users.conversations?user=scopedUserId&types=private_channel"
        SlackUser-->>Route: private channels user is a member of
        Route->>Route: "allowedPrivateChannelIds = Set(user private channels)"
    else legacy credential or BYO bot token
        Route->>Route: use is_member fallback
    end

    Route->>Route: filter private channels against allowedPrivateChannelIds
    Route->>Route: public channels always pass
    Route-->>Client: "{ channels: [...] }"
Loading

Reviews (2): Last reviewed commit: "chore(slack): trim verbose comment" | Re-trigger Greptile

Comment thread apps/sim/app/api/tools/slack/channels/route.ts Outdated
Comment thread apps/sim/app/api/tools/slack/channels/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit be61b2d. Configure here.

@waleedlatif1 waleedlatif1 merged commit 7e0c77c into staging May 28, 2026
13 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/slack-private-channel-privacy branch May 28, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant