Skip to content

Commit a1ef625

Browse files
authored
fix(slack): verify Search permissions before changing active grants (#8545)
* fix(slack): verify Search permissions before changing active grants * fix(slack): bind authorization to every Search approval state * chore(tests): require Redis for Slack authorization integration
1 parent 39067d0 commit a1ef625

9 files changed

Lines changed: 392 additions & 90 deletions

File tree

‎apps/sim/app/o/[organizationId]/integrations/live-member-integrations.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { Chip, toast } from '@sim/emcn'
4+
import { hasSlackSearchUserScopes } from '@/lib/credential-groups/slack-managed-user-scopes'
45
import { LIVE_SEARCH_SCOPE_FIELDS } from '@/lib/sim-search/live/policy-schema'
56
import { liveSearchProviderForCredential } from '@/lib/sim-search/live/provider-catalog'
67
import {
@@ -133,6 +134,8 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
133134
Boolean(option || server) &&
134135
approved &&
135136
(!option || option.configurationStatus === 'ready') &&
137+
(provider !== 'slack' ||
138+
(option?.provider === 'slack' && hasSlackSearchUserScopes(option.requiredScopes))) &&
136139
((provider !== 'hubspot' && provider !== 'zoom') ||
137140
data.availableMcpConnectors.includes(provider))
138141
const scope =

‎apps/sim/app/o/[organizationId]/settings/components/integrations/live-search-settings.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ describe('live search administration', () => {
261261
it('opens Slack setup in Sources instead of its redirected service-account page', async () => {
262262
mocks.policies.mockReturnValue({ data: [{ connectorType: 'slack', approved: true }] })
263263
await render()
264-
await act(async () => button('Slack app')!.click())
264+
await act(async () => button('Verify permissions')!.click())
265265
expect(mockPush).not.toHaveBeenCalled()
266266
expect(container.querySelector('a[href*="providers/slack"]')).toBeNull()
267267
await act(async () =>

‎apps/sim/app/o/[organizationId]/settings/components/integrations/live-search-settings.tsx‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation'
77
import { useQueryState } from 'nuqs'
88
import { SettingsPanel } from '@/components/settings/settings-panel'
99
import type { SearchIntegrationApproval } from '@/lib/api/contracts/knowledge/search-integrations'
10+
import { hasSlackSearchUserScopes } from '@/lib/credential-groups/slack-managed-user-scopes'
1011
import { organizationRoutes } from '@/lib/navigation/paths'
1112
import {
1213
defaultLiveSearchPolicy,
@@ -163,6 +164,14 @@ export function LiveSearchSettings() {
163164
const memberProvider = liveSearchMemberAccountProvider(type)
164165
const mcpProvider = liveSearchMcpConnector(type)
165166
const group = accounts.data?.credentialGroup
167+
const slackOption = group?.options.find((option) => option.provider === 'slack')
168+
const needsSlackSetup =
169+
type === 'slack' &&
170+
accounts.data &&
171+
(group?.status !== 'active' ||
172+
slackOption?.status !== 'active' ||
173+
slackOption.configurationStatus !== 'ready' ||
174+
!hasSlackSearchUserScopes(slackOption.requiredScopes))
166175
const needsMemberSetup =
167176
integration.available !== false &&
168177
accounts.data &&
@@ -193,7 +202,13 @@ export function LiveSearchSettings() {
193202
iconVariant='custom'
194203
icon={<IntegrationTile blockType={type} icon={meta.icon} />}
195204
title={meta.name}
196-
description={integration.available === false ? 'Currently unavailable' : scope}
205+
description={
206+
integration.available === false
207+
? 'Currently unavailable'
208+
: needsSlackSetup
209+
? 'Member accounts · Verify Search permissions'
210+
: scope
211+
}
197212
trailing={
198213
<div className='flex gap-2'>
199214
{serviceAccount && (
@@ -206,7 +221,9 @@ export function LiveSearchSettings() {
206221
</ChipLink>
207222
)}
208223
{type === 'slack' && (
209-
<Chip onClick={() => void setConnectedAccounts('slack')}>Slack app</Chip>
224+
<Chip onClick={() => void setConnectedAccounts('slack')}>
225+
{needsSlackSetup ? 'Verify permissions' : 'Slack app'}
226+
</Chip>
210227
)}
211228
{needsMemberSetup && (
212229
<Chip

‎apps/sim/ee/credential-groups/components/slack-managed-users-modal.tsx‎

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { WorkspaceCredential } from '@/lib/api/contracts'
2020
import type { OrganizationCredential } from '@/lib/api/contracts/organization-credentials'
2121
import { resourceScopeFields, resourceScopeFromOwner } from '@/lib/core/resource-scope'
2222
import {
23+
hasSlackSearchUserScopes,
2324
resolveSlackManagedUserScopes,
2425
SLACK_MANAGED_USER_SCOPES,
2526
SLACK_SEARCH_USER_SCOPES,
@@ -32,6 +33,7 @@ import {
3233
organizationAccountsKeys,
3334
useOrganizationAccounts,
3435
} from '@/hooks/queries/organization-accounts'
36+
import { useSearchIntegrations } from '@/hooks/queries/search-integrations'
3537
import { useSlackSearchInstallations } from '@/hooks/queries/slack-search'
3638
import { credentialGroupKeys } from '@/hooks/queries/utils/credential-group-queries'
3739

@@ -107,11 +109,15 @@ export function SlackManagedUsersModal({
107109
availableApps.find((app) => app.appId === appId) ??
108110
(availableApps.length === 1 && !appId ? availableApps[0] : undefined)
109111
const sharedAppInstalled = organizationSetup && selectedApp?.appKind === 'shared'
112+
const searchPolicies = useSearchIntegrations(organizationId ?? '', {
113+
enabled: open && sharedAppInstalled,
114+
})
115+
const searchApproved = searchPolicies.data?.some(
116+
(policy) => policy.connectorType === 'slack' && policy.approved
117+
)
110118
const accounts = useOrganizationAccounts(open && sharedAppInstalled ? organizationId : undefined)
111119
const memberGroup = accounts.data?.credentialGroup
112-
const memberOption = memberGroup?.options.find(
113-
(option) => option.provider === 'slack' && option.status === 'active'
114-
)
120+
const memberOption = memberGroup?.options.find((option) => option.provider === 'slack')
115121
const sharedAppCanAuthorize = Boolean(
116122
sharedAppInstalled &&
117123
apps.isSuccess &&
@@ -123,11 +129,22 @@ export function SlackManagedUsersModal({
123129
accounts.isSuccess &&
124130
!accounts.isFetching &&
125131
!accounts.error &&
126-
memberGroup?.id === credentialGroupId
132+
searchPolicies.isSuccess &&
133+
!searchPolicies.isFetching &&
134+
!searchPolicies.error &&
135+
memberGroup?.id === credentialGroupId &&
136+
memberOption?.status === 'active'
127137
)
128-
const sharedAppReady = sharedAppCanAuthorize && memberOption?.configurationStatus === 'ready'
138+
const searchPermissionsMissing =
139+
searchApproved && !hasSlackSearchUserScopes(memberOption?.requiredScopes)
140+
const sharedAppReady =
141+
sharedAppCanAuthorize &&
142+
memberOption?.configurationStatus === 'ready' &&
143+
!searchPermissionsMissing
129144
const sharedAppNeedsUpdate =
130-
sharedAppCanAuthorize && memberOption?.configurationStatus === 'needs_update'
145+
sharedAppCanAuthorize &&
146+
(memberOption?.configurationStatus === 'needs_update' ||
147+
(memberOption?.configurationStatus === 'ready' && searchPermissionsMissing))
131148
const [clientId, setClientId] = useState('')
132149
const [clientSecret, setClientSecret] = useState('')
133150
const [pending, setPending] = useState(false)
@@ -389,8 +406,19 @@ export function SlackManagedUsersModal({
389406
const needsApp = organizationSetup && apps.isSuccess && availableApps.length === 0
390407
const checkingSetup =
391408
apps.isPending ||
392-
(sharedAppInstalled && (apps.isFetching || accounts.isPending || accounts.isFetching))
393-
const failedSetup = apps.error ? apps : sharedAppInstalled && accounts.error ? accounts : null
409+
(sharedAppInstalled &&
410+
(apps.isFetching ||
411+
accounts.isPending ||
412+
accounts.isFetching ||
413+
searchPolicies.isPending ||
414+
searchPolicies.isFetching))
415+
const failedSetup = apps.error
416+
? apps
417+
: sharedAppInstalled && searchPolicies.error
418+
? searchPolicies
419+
: sharedAppInstalled && accounts.error
420+
? accounts
421+
: null
394422
const title = organizationSetup ? 'Set up Slack app' : 'Set up Slack'
395423
const primaryLabel = isLoading
396424
? 'Loading...'
@@ -405,7 +433,7 @@ export function SlackManagedUsersModal({
405433
(!organizationSetup && !selectedBot) ||
406434
pending ||
407435
(organizationSetup
408-
? apps.isPending || Boolean(apps.error) || !selectedApp || !requiredScopes.length
436+
? checkingSetup || Boolean(failedSetup) || !selectedApp || !requiredScopes.length
409437
: !clientId.trim() || !clientSecret.trim())
410438

411439
return (
@@ -472,9 +500,9 @@ export function SlackManagedUsersModal({
472500
<p className='text-[var(--text-secondary)] text-sm'>
473501
{sharedAppInstalled
474502
? sharedAppNeedsUpdate
475-
? 'Member access is outdated. Update it so members can reconnect their Slack accounts.'
503+
? 'Verify member permissions. Members will need to reconnect if their app or permissions change.'
476504
: 'The Sim Search installation needs attention. Manage the app to finish setup.'
477-
: 'Verify member authorization for the installed app. Members can then search the Slack conversations they can access.'}
505+
: 'Verify member permissions. Members will need to reconnect if their app or permissions change.'}
478506
</p>
479507
{selectedApp && (
480508
<Chip onClick={() => setAppSetupOpen(true)} disabled={pending}>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ export const completeSlackCredentialGroupConfiguration: OperationUseCase<
169169
attempt.expectedAppId !== pending.expectedAppId ||
170170
attempt.expectedTeamId !== pending.expectedTeamId ||
171171
attempt.appRevision !== pending.appRevision ||
172+
attempt.searchApproval?.approved !== pending.searchApproval?.approved ||
173+
attempt.searchApproval?.updatedAt !== pending.searchApproval?.updatedAt ||
172174
attempt.clientId !== pending.clientId ||
173175
attempt.redirectUri !== pending.redirectUri ||
174176
credentialGroupScopePolicyVersion(attempt.requiredScopes) !==

‎apps/sim/lib/credential-groups/service.ts‎

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ export async function ensureWorkspaceAccountsGroup(
401401
}
402402
}
403403

404-
/** Adds a provider or extends its required consent during an explicit administrator action. */
404+
/** Adds a provider without changing the verified consent policy of existing connections. */
405405
export async function addOrganizationAccountProvider(
406406
organizationId: string,
407407
userId: string,
@@ -430,31 +430,6 @@ export async function addOrganizationAccountProvider(
430430
'validation',
431431
`Enable ${option.label} in Connected accounts first`
432432
)
433-
if (option.requiredScopes) {
434-
const previousScopes =
435-
current.provider === 'slack'
436-
? resolveSlackManagedUserScopes(current.requiredScopes)
437-
: current.requiredScopes
438-
const requiredScopes = [...new Set([...previousScopes, ...option.requiredScopes])]
439-
const scopeVersion = credentialGroupScopePolicyVersion(requiredScopes)
440-
if (!scopesEqual(requiredScopes, previousScopes) || scopeVersion !== current.scopeVersion) {
441-
const [updated] = await executor
442-
.update(credentialGroup)
443-
.set({
444-
options: existing.options.map((entry) =>
445-
entry.id === current.id ? { ...entry, requiredScopes, scopeVersion } : entry
446-
),
447-
updatedAt: new Date(),
448-
})
449-
.where(
450-
and(eq(credentialGroup.id, group.id), resourceScopeCondition(credentialGroup, scope))
451-
)
452-
.returning({ id: credentialGroup.id })
453-
if (!updated) throw new Error('Connected accounts policy update returned no row')
454-
await invalidateOptionGrants(executor, group.id, [current.id])
455-
return { groupId: group.id, changed: true }
456-
}
457-
}
458433
return { groupId: group.id, changed: group.created }
459434
}
460435
if (

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export const SLACK_SEARCH_USER_SCOPES = [
5959
...SLACK_RTS_USER_SCOPES,
6060
] as const
6161

62+
/** Search readiness is separate from a connection's existing workflow permissions. */
63+
export function hasSlackSearchUserScopes(scopes: readonly string[] | undefined): boolean {
64+
return SLACK_SEARCH_USER_SCOPES.every((scope) => scopes?.includes(scope))
65+
}
66+
6267
/** Existing workflow options retain their scope policy; every user grant must attest identity. */
6368
export function resolveSlackManagedUserScopes(requiredScopes?: readonly string[]): string[] {
6469
return [

0 commit comments

Comments
 (0)