Skip to content

Commit 0864b1b

Browse files
committed
fix(slack): honor implicit Search approval during authorization
1 parent a1ef625 commit 0864b1b

3 files changed

Lines changed: 89 additions & 5 deletions

File tree

‎apps/sim/lib/credential-groups/slack-managed-users.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
} from '@/lib/credential-groups/slack-managed-user-scopes'
3434
import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks'
3535
import type { DbOrTx } from '@/lib/db/types'
36+
import { listOrganizationSearchApprovals } from '@/lib/knowledge/search/integration-policy'
3637
import { SLACK_CUSTOM_BOT_PROVIDER_ID, SLACK_CUSTOM_BOT_SECRET_TYPE } from '@/lib/oauth/types'
3738
import { resolveSlackAppCredentials } from '@/lib/slack-search/app-configuration'
3839
import { requireSlackSearchAppAvailable } from '@/lib/slack-search/shared-app'
@@ -579,7 +580,10 @@ export async function createSlackManagedUsersAttempt(params: {
579580
)
580581
.limit(1)
581582
searchApproval = {
582-
approved: approval?.approved ?? false,
583+
approved:
584+
approval?.approved ??
585+
(await listOrganizationSearchApprovals(scope.organizationId)).get('slack') ??
586+
false,
583587
updatedAt: approval?.updatedAt.getTime() ?? null,
584588
}
585589
if (searchApproval.approved)
@@ -837,9 +841,13 @@ export async function exchangeAndConfigureSlackManagedUsers(params: {
837841
)
838842
.limit(1)
839843
.for('share')
844+
const approved =
845+
approval?.approved ??
846+
(await listOrganizationSearchApprovals(params.attempt.organizationId, tx)).get('slack') ??
847+
false
840848
if (
841849
!params.attempt.searchApproval ||
842-
(approval?.approved ?? false) !== params.attempt.searchApproval.approved ||
850+
approved !== params.attempt.searchApproval.approved ||
843851
(approval?.updatedAt.getTime() ?? null) !== params.attempt.searchApproval.updatedAt
844852
)
845853
throw new SlackManagedUsersError(

‎apps/sim/lib/knowledge/__integration__/search-mcp-setup.integration.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,78 @@ describe('atomic organization live Search MCP setup', () => {
332332
restoreSlackHttp = () => spy.mockRestore()
333333
}
334334

335+
async function seedImplicitSlackApproval() {
336+
const knowledgeBaseId = generateId()
337+
const connectorId = generateId()
338+
await db.insert(knowledgeBase).values({
339+
id: knowledgeBaseId,
340+
userId: ids.owner,
341+
organizationId: ids.organization,
342+
isSearchIndex: true,
343+
name: 'Slack Search fixture',
344+
})
345+
await db.insert(knowledgeConnector).values({
346+
id: connectorId,
347+
knowledgeBaseId,
348+
connectorType: 'slack',
349+
status: 'active',
350+
sourceConfig: {},
351+
})
352+
return connectorId
353+
}
354+
355+
it.each([false, true])(
356+
'verifies implicitly approved Search permissions unless explicitly disabled (disabled: %s)',
357+
async (disabled) => {
358+
const setup = await seedSlackAuthorization()
359+
await seedImplicitSlackApproval()
360+
if (disabled)
361+
await approveSearchIntegration.execute({
362+
principal: createSessionPrincipal({ userId: ids.owner, sessionId: generateId() }),
363+
input: { organizationId: ids.organization, connectorType: 'slack', approved: false },
364+
})
365+
expect(await integrationStatus('slack')).toMatchObject({ approved: !disabled })
366+
const pending = await setup.start()
367+
const scopes = disabled
368+
? [...SLACK_MANAGED_USER_SCOPES]
369+
: [...new Set([...SLACK_MANAGED_USER_SCOPES, ...SLACK_SEARCH_USER_SCOPES])]
370+
expect(new URL(pending.authorizationUrl).searchParams.get('user_scope')!.split(',')).toEqual(
371+
expect.arrayContaining(scopes)
372+
)
373+
if (disabled)
374+
expect(new URL(pending.authorizationUrl).searchParams.get('user_scope')).not.toContain(
375+
'search:read.public'
376+
)
377+
provideSlackConsent(scopes)
378+
await expect(setup.complete(pending.state)).resolves.toMatchObject({ ok: true })
379+
const state = await snapshot()
380+
expect(
381+
state.groups[0].options.find((entry) => entry.id === setup.optionId)?.requiredScopes
382+
).toEqual(expect.arrayContaining(scopes))
383+
if (disabled)
384+
await expect(setup.resolveToken()).resolves.toMatchObject({ accessToken: 'fixture-token' })
385+
}
386+
)
387+
388+
it.each(['added', 'removed'] as const)(
389+
'rejects pending authorization when implicit Search approval is %s',
390+
async (change) => {
391+
const setup = await seedSlackAuthorization()
392+
const connectorId = change === 'removed' ? await seedImplicitSlackApproval() : null
393+
const pending = await setup.start()
394+
if (connectorId)
395+
await db
396+
.update(knowledgeConnector)
397+
.set({ archivedAt: new Date() })
398+
.where(eq(knowledgeConnector.id, connectorId))
399+
else await seedImplicitSlackApproval()
400+
provideSlackConsent([...SLACK_MANAGED_USER_SCOPES, ...SLACK_SEARCH_USER_SCOPES])
401+
await expect(setup.complete(pending.state)).rejects.toThrow('Search approval changed')
402+
expect((await snapshot()).groups).toEqual(setup.before.groups)
403+
await expect(setup.resolveToken()).resolves.toMatchObject({ accessToken: 'fixture-token' })
404+
}
405+
)
406+
335407
it.each([
336408
{ name: 'workflow policy', scopes: SLACK_MANAGED_USER_SCOPES },
337409
{ name: 'custom policy', scopes: ['chat:write', 'users:read', 'users:read.email'] },

‎apps/sim/lib/knowledge/search/integration-policy.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@ import { db } from '@sim/db'
22
import { knowledgeBase, knowledgeConnector, organizationSearchIntegration } from '@sim/db/schema'
33
import { and, eq, isNull, sql } from 'drizzle-orm'
44
import { OrchestrationError } from '@/lib/core/orchestration/types'
5+
import type { DbOrTx } from '@/lib/db/types'
56

67
/** Existing configured sources retain approval until an admin records an explicit decision. */
7-
export async function listOrganizationSearchApprovals(organizationId: string) {
8+
export async function listOrganizationSearchApprovals(
9+
organizationId: string,
10+
executor: DbOrTx = db
11+
) {
812
const [decisions, configured] = await Promise.all([
9-
db
13+
executor
1014
.select({
1115
connectorType: organizationSearchIntegration.connectorType,
1216
approved: organizationSearchIntegration.approved,
1317
})
1418
.from(organizationSearchIntegration)
1519
.where(eq(organizationSearchIntegration.organizationId, organizationId)),
16-
db
20+
executor
1721
.selectDistinct({ connectorType: knowledgeConnector.connectorType })
1822
.from(knowledgeConnector)
1923
.innerJoin(knowledgeBase, eq(knowledgeBase.id, knowledgeConnector.knowledgeBaseId))

0 commit comments

Comments
 (0)