Skip to content

Commit 98e34f4

Browse files
authored
fix(slack): connect Slack member accounts without the Sim Search bot (#8979)
* fix(slack): connect Slack member accounts without the Sim Search bot * fix(slack): revoke shared grants on stale uninstall, pair app and workspace ids, audit text * fix(slack): cancel stale-uninstall turns for this installation's revoked members
1 parent 496fd61 commit 98e34f4

14 files changed

Lines changed: 745 additions & 164 deletions

File tree

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

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,27 @@ export function SlackManagedUsersModal({
109109
availableApps.find((app) => app.appId === appId) ??
110110
(availableApps.length === 1 && !appId ? availableApps[0] : undefined)
111111
const sharedAppInstalled = organizationSetup && selectedApp?.appKind === 'shared'
112+
const noInstalledApp = organizationSetup && apps.isSuccess && availableApps.length === 0
113+
/** The shared app connects member accounts without a Search bot installation. */
114+
const sharedAppWithoutBot = noInstalledApp && Boolean(apps.data?.sharedAppAvailable)
115+
const sharedSetup = sharedAppInstalled || sharedAppWithoutBot
112116
const searchPolicies = useSearchIntegrations(organizationId ?? '', {
113-
enabled: open && sharedAppInstalled,
117+
enabled: open && sharedSetup,
114118
})
115119
const searchApproved = searchPolicies.data?.some(
116120
(policy) => policy.connectorType === 'slack' && policy.approved
117121
)
118-
const accounts = useOrganizationAccounts(open && sharedAppInstalled ? organizationId : undefined)
122+
const accounts = useOrganizationAccounts(open && sharedSetup ? organizationId : undefined)
119123
const memberGroup = accounts.data?.credentialGroup
120124
const memberOption = memberGroup?.options.find((option) => option.provider === 'slack')
125+
const memberSetupSettled =
126+
accounts.isSuccess &&
127+
!accounts.isFetching &&
128+
!accounts.error &&
129+
searchPolicies.isSuccess &&
130+
!searchPolicies.isFetching &&
131+
!searchPolicies.error &&
132+
memberGroup?.id === credentialGroupId
121133
const sharedAppCanAuthorize = Boolean(
122134
sharedAppInstalled &&
123135
apps.isSuccess &&
@@ -126,19 +138,18 @@ export function SlackManagedUsersModal({
126138
apps.data?.sharedAppAvailable &&
127139
selectedApp.enabled &&
128140
!selectedApp.needsValidation &&
129-
accounts.isSuccess &&
130-
!accounts.isFetching &&
131-
!accounts.error &&
132-
searchPolicies.isSuccess &&
133-
!searchPolicies.isFetching &&
134-
!searchPolicies.error &&
135-
memberGroup?.id === credentialGroupId &&
141+
memberSetupSettled &&
136142
memberOption?.status === 'active'
137143
)
144+
const memberOnlyCanAuthorize =
145+
sharedAppWithoutBot &&
146+
!apps.isFetching &&
147+
memberSetupSettled &&
148+
(!memberOption || memberOption.status === 'active')
138149
const searchPermissionsMissing =
139150
searchApproved && !hasSlackSearchUserScopes(memberOption?.requiredScopes)
140151
const sharedAppReady =
141-
sharedAppCanAuthorize &&
152+
(sharedAppCanAuthorize || memberOnlyCanAuthorize) &&
142153
memberOption?.configurationStatus === 'ready' &&
143154
!searchPermissionsMissing
144155
const sharedAppNeedsUpdate =
@@ -176,7 +187,7 @@ export function SlackManagedUsersModal({
176187
: 'search')
177188
const requiredScopes =
178189
scope.kind === 'organization'
179-
? selectedApp
190+
? selectedApp || sharedAppWithoutBot
180191
? [...SLACK_SEARCH_USER_SCOPES]
181192
: []
182193
: access === null
@@ -300,15 +311,24 @@ export function SlackManagedUsersModal({
300311
if (
301312
pending ||
302313
(sharedAppInstalled && !sharedAppNeedsUpdate) ||
314+
(sharedAppWithoutBot && !memberOnlyCanAuthorize) ||
303315
(!organizationSetup && !selectedBot)
304316
)
305317
return
306-
if (
307-
organizationSetup
308-
? !selectedApp || !requiredScopes.length
309-
: !clientId.trim() || !clientSecret.trim()
310-
)
318+
if (organizationSetup ? !requiredScopes.length : !clientId.trim() || !clientSecret.trim())
311319
return
320+
const body = {
321+
...(organizationSetup
322+
? selectedApp
323+
? { appId: selectedApp.appId, teamId: selectedApp.teamId }
324+
: {}
325+
: {
326+
slackBotCredentialId: selectedBot?.id,
327+
clientId: clientId.trim(),
328+
clientSecret: clientSecret.trim(),
329+
}),
330+
requiredScopes,
331+
}
312332

313333
if (isDesktopApp()) {
314334
const controller = new AbortController()
@@ -320,16 +340,7 @@ export function SlackManagedUsersModal({
320340
kind: 'slack-managed-users',
321341
owner: resourceScopeFields(scope),
322342
credentialGroupId,
323-
body: {
324-
...(organizationSetup
325-
? { appId: selectedApp?.appId, teamId: selectedApp?.teamId }
326-
: {
327-
slackBotCredentialId: selectedBot?.id,
328-
clientId: clientId.trim(),
329-
clientSecret: clientSecret.trim(),
330-
}),
331-
requiredScopes,
332-
},
343+
body,
333344
},
334345
controller.signal
335346
)
@@ -365,16 +376,7 @@ export function SlackManagedUsersModal({
365376
const result = await startAuthorization.mutateAsync({
366377
...resourceScopeFields(scope),
367378
credentialGroupId,
368-
body: {
369-
...(organizationSetup
370-
? { appId: selectedApp?.appId, teamId: selectedApp?.teamId }
371-
: {
372-
slackBotCredentialId: selectedBot?.id,
373-
clientId: clientId.trim(),
374-
clientSecret: clientSecret.trim(),
375-
}),
376-
requiredScopes,
377-
},
379+
body,
378380
})
379381
if (popup.current !== opened) return
380382
expectedState.current = result.state
@@ -403,28 +405,28 @@ export function SlackManagedUsersModal({
403405
if (sharedAppReady && !appSetupOpen) return null
404406

405407
const noBots = !organizationSetup && !isLoading && bots.length === 0
406-
const needsApp = organizationSetup && apps.isSuccess && availableApps.length === 0
408+
const needsApp = noInstalledApp && !sharedAppWithoutBot
407409
const checkingSetup =
408410
apps.isPending ||
409-
(sharedAppInstalled &&
411+
(sharedSetup &&
410412
(apps.isFetching ||
411413
accounts.isPending ||
412414
accounts.isFetching ||
413415
searchPolicies.isPending ||
414416
searchPolicies.isFetching))
415417
const failedSetup = apps.error
416418
? apps
417-
: sharedAppInstalled && searchPolicies.error
419+
: sharedSetup && searchPolicies.error
418420
? searchPolicies
419-
: sharedAppInstalled && accounts.error
421+
: sharedSetup && accounts.error
420422
? accounts
421423
: null
422424
const title = organizationSetup ? 'Set up Slack app' : 'Set up Slack'
423425
const primaryLabel = isLoading
424426
? 'Loading...'
425427
: pending
426428
? 'Waiting for Slack...'
427-
: sharedAppNeedsUpdate
429+
: sharedAppNeedsUpdate || (sharedAppWithoutBot && memberOption)
428430
? 'Update member access'
429431
: 'Verify and add'
430432
const primaryDisabled =
@@ -433,7 +435,10 @@ export function SlackManagedUsersModal({
433435
(!organizationSetup && !selectedBot) ||
434436
pending ||
435437
(organizationSetup
436-
? checkingSetup || Boolean(failedSetup) || !selectedApp || !requiredScopes.length
438+
? checkingSetup ||
439+
Boolean(failedSetup) ||
440+
(sharedAppWithoutBot && !memberOnlyCanAuthorize) ||
441+
!requiredScopes.length
437442
: !clientId.trim() || !clientSecret.trim())
438443

439444
return (
@@ -465,6 +470,14 @@ export function SlackManagedUsersModal({
465470
Retry
466471
</Chip>
467472
</ChipModalField>
473+
) : sharedAppWithoutBot ? (
474+
<ChipModalField type='custom' title='Member accounts'>
475+
<p className='text-[var(--text-secondary)] text-sm'>
476+
{memberOption
477+
? 'Verify member permissions. Members will need to reconnect if their app or permissions change.'
478+
: 'Verify member permissions in your Slack workspace.'}
479+
</p>
480+
</ChipModalField>
468481
) : needsApp ? (
469482
<ChipModalField type='custom' title='Install Sim Search first'>
470483
<p className='text-[var(--text-secondary)] text-sm'>

‎apps/sim/hooks/queries/credential-groups.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ export function useStartSlackCredentialGroupConfiguration() {
4242
)) => {
4343
const scope = resourceScopeFromOwner({ workspaceId, organizationId })
4444
if (scope.kind === 'organization') {
45-
if (!body.appId || !body.teamId)
46-
throw new Error('Slack App ID and workspace ID are required')
4745
return requestJson(startOrganizationSlackConfigurationContract, {
4846
params: { id: scope.organizationId, groupId: credentialGroupId },
4947
body: {

‎apps/sim/lib/api/contracts/organization-accounts.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,13 @@ export const startOrganizationSlackConfigurationContract = defineRouteContract({
130130
method: 'POST',
131131
path: '/api/organizations/[id]/connected-accounts/[groupId]/slack-managed-users',
132132
params: organizationAccountsParamsSchema.extend({ groupId: z.string().min(1).max(128) }),
133+
/** Omitting `appId` selects the shared Slack app for an organization with no installed bot. */
133134
body: startSlackCredentialGroupConfigurationBodySchema
134135
.omit({ slackBotCredentialId: true, clientId: true, clientSecret: true })
135-
.required({ appId: true, teamId: true }),
136+
.refine((body) => (body.appId === undefined) === (body.teamId === undefined), {
137+
message: 'Select the Slack app and its workspace together',
138+
path: ['appId'],
139+
}),
136140
response: startSlackCredentialGroupConfigurationContract.response,
137141
})
138142
export type OrganizationAccountsSettings = z.output<

0 commit comments

Comments
 (0)