Skip to content

Commit b8d9902

Browse files
fix(slack-search): align custom and shared app permissions (#8109)
* fix(slack-search): grant custom bots channel listing scopes * fix(slack-search): include full scope sets in custom manifests
1 parent 96090e0 commit b8d9902

6 files changed

Lines changed: 111 additions & 79 deletions

File tree

‎apps/docs/content/docs/search/slack.mdx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,11 @@ To switch apps, first remove Slack connections under **Settings → Sources →
115115

116116
## Permissions reference
117117

118-
New Search member connections request these read-only **User Token Scopes**. DM scopes are requested even when DM indexing is off; the source settings determine what is indexed. Bot scopes are separate and allow Sim to receive and answer questions in Slack.
118+
New Search member connections request these read-only **User Token Scopes**. DM scopes are requested even when DM indexing is off; the source settings determine what is indexed. Bot scopes are separate and allow Sim to receive and answer questions in Slack and list channels during source setup.
119+
120+
The custom app's **Bot Token Scopes** include `channels:read` and `groups:read` for the channel picker. Private channels appear only when the bot has access. For an existing app, add these scopes under **OAuth & Permissions**, reinstall the app in Slack to approve the changes, then reconnect the bot in Sim.
121+
122+
Custom and official app manifests declare the same full bot and user scope sets, including permissions reserved for additional capabilities. The table below lists the scopes requested by member indexing; declaring additional user scopes in the manifest does not automatically grant them to each member connection.
119123

120124
| Purpose | User scopes |
121125
|---|---|
@@ -146,5 +150,6 @@ See Slack's [app manifest reference](https://docs.slack.dev/reference/app-manife
146150
| Redirect mismatch | Check all three redirect URLs above against your Sim origin. |
147151
| App or workspace mismatch | Use the App ID and client credentials from the same app, and the ID of the workspace being authorized. |
148152
| Missing scopes | Compare User Token Scopes with the table above, update the Slack app, reinstall as Slack requires, and reconnect. In workspace setup, select **Search documents** in both setup screens. |
153+
| Channel picker says Options unavailable | Check that the selected custom bot has `channels:read` and `groups:read` under Bot Token Scopes. After adding them, reinstall the app in Slack and reconnect the bot in Sim. |
149154
| Missing private-channel results | Confirm the member is in the channel and it is within the source filters. With an indexing account, confirm that account can read it too. |
150155
| Slow initial indexing | Check sync status and Slack rate limits. A large history can take multiple background runs. |

‎apps/sim/lib/internal/slack/oauth.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ describe('Slack bot grant policy and cleanup', () => {
7676
it('accepts the existing indexing bot scope policy', () => {
7777
expect(() => validateSlackBotAuthorization(grant)).not.toThrow()
7878
})
79+
it.each(['channels:read', 'groups:read'] as const)(
80+
'rejects a bot grant missing channel picker scope %s',
81+
(missingScope) => {
82+
expect(() =>
83+
validateSlackBotAuthorization({
84+
...grant,
85+
scope: SLACK_SEARCH_SCOPES.filter((scope) => scope !== missingScope).join(','),
86+
})
87+
).toThrow(`Reinstall the app with these scopes: ${missingScope}`)
88+
}
89+
)
7990
it('requires the additional command scope for shared installs', () => {
8091
expect(() =>
8192
validateSlackBotAuthorization(grant, [...SLACK_SEARCH_SCOPES, 'commands'])

‎apps/sim/lib/internal/slack/search-client.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ describe('Slack Search provider verification', () => {
4545
fetchMock.mockResolvedValue(new Response(JSON.stringify(auth)))
4646
await expect(verifySlackSearchBot('token')).rejects.toThrow('Reinstall')
4747
})
48+
it.each(['channels:read', 'groups:read'] as const)(
49+
'rejects an installed bot missing channel picker scope %s',
50+
async (missingScope) => {
51+
fetchMock.mockResolvedValue(
52+
reply(
53+
auth,
54+
SLACK_SEARCH_SCOPES.filter((scope) => scope !== missingScope)
55+
)
56+
)
57+
await expect(verifySlackSearchBot('token')).rejects.toThrow(
58+
`Reinstall the Slack bot with these scopes: ${missingScope}`
59+
)
60+
expect(fetchMock).toHaveBeenCalledOnce()
61+
}
62+
)
4863
it.each([
4964
{ deleted: true },
5065
{ is_bot: true },

‎apps/sim/lib/slack-search/constants.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export const SLACK_SEARCH_SCOPES = [
22
'assistant:write',
33
'chat:write',
4+
'channels:read',
5+
'groups:read',
46
'im:history',
57
'im:write',
68
'app_mentions:read',

‎apps/sim/lib/slack-search/manifest.test.ts‎

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ describe('Search app manifest', () => {
1515
'app_mentions:read',
1616
'im:write',
1717
'im:history',
18+
'channels:read',
19+
'groups:read',
1820
])
1921
)
20-
expect(manifest.oauth_config.scopes.bot).not.toContain('groups:history')
2122
expect(manifest.oauth_config.scopes.user).toEqual(
2223
expect.arrayContaining([
2324
'users:read',
@@ -47,26 +48,21 @@ describe('Search app manifest', () => {
4748
).toBe(true)
4849
})
4950
it('preserves existing member grants when updating a bot manifest', () => {
50-
expect(
51-
createSlackSearchManifest('Sim Search', 'Search', 'https://sim.test', ['files:read'])
52-
.oauth_config.scopes.user
53-
).toContain('files:read')
51+
const manifest = createSlackSearchManifest('Sim Search', 'Search', 'https://sim.test', [
52+
'files:read',
53+
'files:write',
54+
])
55+
expect(manifest.oauth_config.scopes.user).toContain('files:write')
56+
expect(manifest.oauth_config.scopes.user.filter((scope) => scope === 'files:read')).toEqual([
57+
'files:read',
58+
])
5459
})
55-
it('uses one origin for unified ingress and OAuth with only the required bot permissions', () => {
60+
it('uses one origin for unified ingress and OAuth', () => {
5661
const manifest = createSlackSearchManifest(
5762
'Sim Search',
5863
'Search with sources',
5964
'https://search-test.ngrok.app'
6065
)
61-
expect(manifest.oauth_config.scopes.bot).toEqual([
62-
'assistant:write',
63-
'chat:write',
64-
'im:history',
65-
'im:write',
66-
'app_mentions:read',
67-
'users:read',
68-
'users:read.email',
69-
])
7066
expect(manifest.settings.event_subscriptions.request_url).toBe(
7167
'https://search-test.ngrok.app/api/webhooks/slack'
7268
)
@@ -92,23 +88,26 @@ describe('Search app manifest', () => {
9288
})
9389
})
9490

95-
it('official app declares expanded permissions without subscribing to member message events', () => {
96-
const manifest = createSharedSlackSearchManifest('https://www.sim.ai')
97-
expect(manifest.oauth_config.scopes.user).toEqual([
91+
it.each([
92+
{
93+
name: 'custom',
94+
manifest: createSlackSearchManifest('Sim Search', 'Search', 'https://sim.test'),
95+
},
96+
{ name: 'shared', manifest: createSharedSlackSearchManifest('https://sim.test') },
97+
])('$name app declares the complete bot and user scope sets without duplicates', ({ manifest }) => {
98+
expect([...manifest.oauth_config.scopes.user].sort()).toEqual([
99+
'canvases:read',
100+
'canvases:write',
98101
'channels:history',
99102
'channels:read',
103+
'chat:write',
104+
'files:read',
100105
'groups:history',
101106
'groups:read',
102107
'im:history',
103108
'im:read',
104109
'mpim:history',
105110
'mpim:read',
106-
'users:read',
107-
'users:read.email',
108-
'canvases:read',
109-
'canvases:write',
110-
'chat:write',
111-
'files:read',
112111
'search:read.files',
113112
'search:read.im',
114113
'search:read.mpim',
@@ -117,32 +116,38 @@ it('official app declares expanded permissions without subscribing to member mes
117116
'search:read.users',
118117
'team:read',
119118
'usergroups:read',
120-
])
121-
expect(manifest.oauth_config.scopes.bot).toEqual([
122-
'assistant:write',
123-
'chat:write',
124-
'im:history',
125-
'im:write',
126-
'app_mentions:read',
127119
'users:read',
128120
'users:read.email',
129-
'commands',
121+
])
122+
expect([...manifest.oauth_config.scopes.bot].sort()).toEqual([
123+
'app_mentions:read',
124+
'assistant:write',
130125
'channels:history',
131126
'channels:manage',
132127
'channels:read',
133128
'channels:write.invites',
129+
'chat:write',
134130
'chat:write.public',
131+
'commands',
135132
'groups:history',
136133
'groups:read',
137134
'groups:write',
138135
'groups:write.invites',
136+
'im:history',
137+
'im:write',
139138
'links:read',
140139
'links:write',
141140
'mpim:history',
142141
'mpim:read',
143142
'mpim:write',
144143
'reactions:write',
144+
'users:read',
145+
'users:read.email',
145146
])
147+
})
148+
149+
it('official app declares commands and lifecycle events without member message events', () => {
150+
const manifest = createSharedSlackSearchManifest('https://www.sim.ai')
146151
expect(manifest.features.slash_commands.map((command) => command.command)).toEqual([
147152
'/query',
148153
'/connect',

‎apps/sim/lib/slack-search/manifest.ts‎

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import {
33
SLACK_MANAGED_USER_ENROLLMENT_CALLBACK_PATH,
44
SLACK_SEARCH_USER_SCOPES,
55
} from '@/lib/credential-groups/slack-managed-user-scopes'
6-
import { SLACK_SEARCH_SCOPES, SLACK_SHARED_SEARCH_BOT_SCOPES } from '@/lib/slack-search/constants'
6+
import { SLACK_SHARED_SEARCH_BOT_SCOPES } from '@/lib/slack-search/constants'
77

88
export const SLACK_SEARCH_CALLBACK_PATH = '/api/knowledge/slack/oauth/callback'
99
export const SLACK_SEARCH_WEBHOOK_PATH = '/api/webhooks/slack'
1010
export const SLACK_SEARCH_DEFAULT_NAME = 'Sim Search'
1111
export const SLACK_SEARCH_DEFAULT_DESCRIPTION =
1212
'Ask questions about your organization’s knowledge and get answers with sources.'
1313

14-
/** Bot conversations and member indexing share one manifest and app identity. */
14+
/**
15+
* Custom and shared apps declare the same permissions, including planned capabilities.
16+
* Runtime OAuth validation requires only scopes used by implemented features.
17+
*/
1518
export function createSlackSearchManifest(
1619
name: string,
1720
description: string,
@@ -38,8 +41,40 @@ export function createSlackSearchManifest(
3841
SLACK_MANAGED_USER_ENROLLMENT_CALLBACK_PATH,
3942
].map((path) => new URL(path, url).href),
4043
scopes: {
41-
bot: [...SLACK_SEARCH_SCOPES],
42-
user: [...new Set([...SLACK_SEARCH_USER_SCOPES, ...existingUserScopes])],
44+
bot: [
45+
...SLACK_SHARED_SEARCH_BOT_SCOPES,
46+
'channels:history',
47+
'channels:manage',
48+
'channels:write.invites',
49+
'chat:write.public',
50+
'groups:history',
51+
'groups:write',
52+
'groups:write.invites',
53+
'links:read',
54+
'links:write',
55+
'mpim:history',
56+
'mpim:read',
57+
'mpim:write',
58+
'reactions:write',
59+
],
60+
user: [
61+
...new Set([
62+
...SLACK_SEARCH_USER_SCOPES,
63+
'canvases:read',
64+
'canvases:write',
65+
'chat:write',
66+
'files:read',
67+
'search:read.files',
68+
'search:read.im',
69+
'search:read.mpim',
70+
'search:read.private',
71+
'search:read.public',
72+
'search:read.users',
73+
'team:read',
74+
'usergroups:read',
75+
...existingUserScopes,
76+
]),
77+
],
4378
},
4479
},
4580
settings: {
@@ -55,10 +90,7 @@ export function createSlackSearchManifest(
5590
}
5691
}
5792

58-
/**
59-
* Declares the company app's permissions, including planned capabilities.
60-
* Runtime OAuth validation continues to require only scopes used by implemented features.
61-
*/
93+
/** Adds the official app's commands and lifecycle events to the common manifest. */
6294
export function createSharedSlackSearchManifest(origin: string) {
6395
const manifest = createSlackSearchManifest(
6496
SLACK_SEARCH_DEFAULT_NAME,
@@ -87,44 +119,6 @@ export function createSharedSlackSearchManifest(origin: string) {
87119
},
88120
],
89121
},
90-
oauth_config: {
91-
...manifest.oauth_config,
92-
scopes: {
93-
bot: [
94-
...SLACK_SHARED_SEARCH_BOT_SCOPES,
95-
'channels:history',
96-
'channels:manage',
97-
'channels:read',
98-
'channels:write.invites',
99-
'chat:write.public',
100-
'groups:history',
101-
'groups:read',
102-
'groups:write',
103-
'groups:write.invites',
104-
'links:read',
105-
'links:write',
106-
'mpim:history',
107-
'mpim:read',
108-
'mpim:write',
109-
'reactions:write',
110-
],
111-
user: [
112-
...SLACK_SEARCH_USER_SCOPES,
113-
'canvases:read',
114-
'canvases:write',
115-
'chat:write',
116-
'files:read',
117-
'search:read.files',
118-
'search:read.im',
119-
'search:read.mpim',
120-
'search:read.private',
121-
'search:read.public',
122-
'search:read.users',
123-
'team:read',
124-
'usergroups:read',
125-
],
126-
},
127-
},
128122
settings: {
129123
...manifest.settings,
130124
event_subscriptions: {

0 commit comments

Comments
 (0)