Skip to content

Commit 4f3afe7

Browse files
committed
fix(slack): refuse a custom shared-app row at setup start, scope lock barriers, assert conflict code
1 parent 52a3386 commit 4f3afe7

4 files changed

Lines changed: 39 additions & 25 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/changelog-empty-state.tsx‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ interface Release {
2424

2525
const LATEST: Release = { top: 10, notes: [118, 92] }
2626

27-
/** Every release ends inside the 148px viewBox; the fade carries the timeline past it. */
28-
const EARLIER: readonly Release[] = [{ top: 70, notes: [104, 126, 80] }] as const
27+
/** Ends inside the 148px viewBox; the fade carries the timeline past it. */
28+
const EARLIER: Release = { top: 70, notes: [104, 126, 80] }
2929

3030
const SPINE_X = 24
3131
const NOTE_PITCH = 12
@@ -84,15 +84,11 @@ function ChangelogGraphic() {
8484
<rect x='92' y={LATEST.top + 6.5} width='34' height='4' rx='2' fill={INK.detail} />
8585
<ReleaseNotes release={LATEST} />
8686

87-
{EARLIER.map((release) => (
88-
<g key={release.top}>
89-
<circle cx={SPINE_X} cy={release.top + 8} r='4' fill={INK.title} />
90-
<rect x='42' y={release.top + 2} width='40' height='13' rx='6.5' fill={INK.detail} />
91-
<rect x='50' y={release.top + 6.5} width='24' height='4' rx='2' fill={INK.title} />
92-
<rect x='92' y={release.top + 6.5} width='34' height='4' rx='2' fill={INK.detail} />
93-
<ReleaseNotes release={release} />
94-
</g>
95-
))}
87+
<circle cx={SPINE_X} cy={EARLIER.top + 8} r='4' fill={INK.title} />
88+
<rect x='42' y={EARLIER.top + 2} width='40' height='13' rx='6.5' fill={INK.detail} />
89+
<rect x='50' y={EARLIER.top + 6.5} width='24' height='4' rx='2' fill={INK.title} />
90+
<rect x='92' y={EARLIER.top + 6.5} width='34' height='4' rx='2' fill={INK.detail} />
91+
<ReleaseNotes release={EARLIER} />
9692
</svg>
9793
)
9894
}

‎apps/sim/lib/credential-groups/__integration__/slack-shared-member-accounts.integration.ts‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,20 @@ describe('Slack member accounts on the shared app without a Search bot', () => {
222222
locked.resolve(connection.pid)
223223
await release.promise
224224
})
225-
const holderPid = await locked.promise
225+
const holderPid = await Promise.race([
226+
locked.promise,
227+
holder.then(() => {
228+
throw new Error('The lock holder finished before holding the lock')
229+
}),
230+
])
226231
const contender = contend()
227232
contender.catch(() => undefined)
228233
try {
229234
await vi.waitFor(
230235
async () => {
231236
const [waiting] = await db.execute<{ pid: number }>(
232-
sql`SELECT pid FROM pg_stat_activity WHERE ${holderPid} = ANY(pg_blocking_pids(pid))`
237+
sql`SELECT pid FROM pg_stat_activity
238+
WHERE ${holderPid} = ANY(pg_blocking_pids(pid)) AND query LIKE '%lock=''slack_org_app''%'`
233239
)
234240
expect(waiting).toBeDefined()
235241
},
@@ -406,9 +412,11 @@ describe('Slack member accounts on the shared app without a Search bot', () => {
406412
).resolves.toEqual({ id: installationId })
407413
})
408414

409-
it('explains a conflict when a custom app row holds the shared app ID', async () => {
415+
it('refuses member setup when a custom app row holds the shared app ID', async () => {
410416
const groupId = (await approveSlack()).memberAccounts?.groupId
411417
if (!groupId) throw new Error('Approval did not provision organization accounts')
418+
const pending = new URL((await startSetup(groupId)).authorizationUrl)
419+
const before = await accountsGroup()
412420
const secret = (await encryptSecret('fixture-custom')).encrypted
413421
await db.insert(slackApp).values({
414422
id: SHARED_APP_ID,
@@ -419,12 +427,12 @@ describe('Slack member accounts on the shared app without a Search bot', () => {
419427
encryptedSigningSecret: secret,
420428
revision: generateId(),
421429
})
422-
const pending = new URL((await startSetup(groupId)).authorizationUrl)
423-
const before = await accountsGroup()
424430
provideSlackConsent(MEMBER_TEAM)
425-
await expect(completeSetup(pending.searchParams.get('state') ?? '')).rejects.toThrow(
426-
'belongs to another installation owner'
427-
)
431+
await expect(completeSetup(pending.searchParams.get('state') ?? '')).rejects.toMatchObject({
432+
code: 'conflict',
433+
message: 'This Slack app belongs to another installation owner',
434+
})
435+
await expect(startSetup(groupId)).rejects.toThrow('belongs to another installation owner')
428436
expect(await accountsGroup()).toEqual(before)
429437
const [row] = await db.select().from(slackApp).where(eq(slackApp.id, SHARED_APP_ID))
430438
expect(row).toMatchObject({ kind: 'custom', organizationId: ids.organization })

‎apps/sim/lib/credential-groups/organization-slack-app.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks'
1010
import type { DbOrTx, DbTransaction } from '@/lib/db/types'
1111

1212
/**
13-
* Serializes an organization's Slack member setup with Slack Search's own installation writes
14-
* (install, rebind, enable, disable, removal), so neither commits against an app or workspace the
15-
* other changed. Taken before any Slack app or workspace lock. Slack revocations, which must never
16-
* wait on setup, and the cascade from deleting a bot credential skip it: they only withdraw a bot,
17-
* and member setup stays valid without one.
13+
* Serializes an organization's Slack member setup with Slack Search's installation writes, so a
14+
* bot can never be added or rebound under a member setup that already checked the installations.
15+
* Install, rebind, enable, disable and admin removal all take it, first, before any Slack app or
16+
* workspace lock. Slack revocations, which must never wait on setup, and the cascade from
17+
* deleting a bot credential skip it: they only withdraw a bot, which member setup tolerates.
1818
*/
1919
export async function lockOrganizationSlackApp(tx: DbTransaction, organizationId: string) {
2020
await acquireAdvisoryXactLock(tx, 'slack_org_app', `slack-org-app:${organizationId}`)

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,16 @@ async function resolveSharedSlackMemberApp(
569569
'Set up this organization’s Slack app first.',
570570
'invalid_response'
571571
)
572+
const [appRow] = await db
573+
.select({ kind: slackApp.kind, organizationId: slackApp.organizationId })
574+
.from(slackApp)
575+
.where(eq(slackApp.id, app.id))
576+
.limit(1)
577+
if (appRow && (appRow.kind !== 'shared' || appRow.organizationId !== null))
578+
throw new SlackManagedUsersError(
579+
'This Slack app belongs to another installation owner.',
580+
'invalid_response'
581+
)
572582
if (installation)
573583
throw new SlackManagedUsersError(
574584
'Select the organization’s installed Slack app.',
@@ -901,7 +911,7 @@ export async function exchangeAndConfigureSlackManagedUsers(params: {
901911
)
902912
.limit(1)
903913
.for('update')
904-
/** A custom row holding the shared app's ID belongs to another owner; never adopt it. */
914+
/** Start refuses a custom row holding the shared app's ID; this covers one added since. */
905915
if (sharedApp && app?.kind !== 'shared')
906916
throw new OrchestrationError(
907917
'conflict',

0 commit comments

Comments
 (0)