Skip to content

Commit ae49070

Browse files
fix(slack): stop requesting the unapproved scopes, gate them behind an opt-in flag (#5977)
* fix(slack): stop requesting the unapproved mention/assistant/DM scopes * feat(slack): gate the approval-pending scopes behind an opt-in env flag
1 parent bd61603 commit ae49070

5 files changed

Lines changed: 40 additions & 26 deletions

File tree

apps/sim/blocks/blocks/slack.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,6 @@ import { normalizeFileInput } from '@/blocks/utils'
77
import type { SlackResponse } from '@/tools/slack/types'
88
import { getTrigger } from '@/triggers'
99

10-
/**
11-
* Scopes added for the native Sim Slack app trigger (`slack_oauth`): the shared
12-
* app's `app_mention`, assistant-thread, and DM events don't deliver without
13-
* them. Advertised by `slack_v2` and the trigger only — the legacy block has no
14-
* feature that needs them, and listing them there would flag every existing
15-
* Slack credential as missing scopes and prompt a reconnect.
16-
*
17-
* This only controls what each picker *advertises* and treats as missing. The
18-
* authorization request itself is provider-wide
19-
* (`getCanonicalScopesForProvider('slack')`), so any reconnect grants the full
20-
* set regardless of which block started it.
21-
*/
22-
const SLACK_V2_ONLY_SCOPES = new Set(['app_mentions:read', 'assistant:write', 'im:history'])
23-
24-
/** Slack scopes the legacy v1 block advertises — the set from before the trigger expansion. */
25-
const SLACK_V1_ADVERTISED_SCOPES = getScopesForService('slack').filter(
26-
(scope) => !SLACK_V2_ONLY_SCOPES.has(scope)
27-
)
28-
2910
export const SlackBlock: BlockConfig<SlackResponse> = {
3011
type: 'slack',
3112
name: 'Slack',
@@ -126,7 +107,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
126107
canonicalParamId: 'oauthCredential',
127108
mode: 'basic',
128109
serviceId: 'slack',
129-
requiredScopes: SLACK_V1_ADVERTISED_SCOPES,
110+
requiredScopes: getScopesForService('slack'),
130111
placeholder: 'Select Slack workspace',
131112
dependsOn: ['authMethod'],
132113
condition: {
@@ -2661,9 +2642,6 @@ function adaptSubBlockForV2(sb: SubBlockConfig): SubBlockConfig {
26612642
...rest,
26622643
credentialKind: 'any',
26632644
placeholder: 'Select Slack account or bot',
2664-
// Full set, unlike v1: v2 hosts the native Sim app trigger, whose events
2665-
// need the mention/assistant/DM scopes.
2666-
requiredScopes: getScopesForService('slack'),
26672645
credentialLabels: {
26682646
oauthGroup: 'Sim app',
26692647
oauthConnect: 'Connect the Sim app',

apps/sim/lib/core/config/env-flags.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,28 @@ export const isSignupMxValidationEnabled = isTruthy(env.SIGNUP_MX_VALIDATION_ENA
144144
export const isAppConfigEnabled =
145145
isHosted && Boolean(env.APPCONFIG_APPLICATION && env.APPCONFIG_ENVIRONMENT)
146146

147+
/**
148+
* Whether the deployment's Slack app is approved for `app_mentions:read`,
149+
* `assistant:write`, and `im:history` — the scopes backing the native Sim app
150+
* trigger's mention, assistant-thread, and DM events.
151+
*
152+
* Off by default because Slack rejects the ENTIRE authorization when it requests
153+
* a scope the app is not approved for, breaking every Slack connect. Sim Cloud's
154+
* app is directory-listed and pinned to its review-approved set, so this stays
155+
* off there until review lands; a self-hosted deployment pointing at its own
156+
* (unlisted) Slack app can opt in.
157+
*
158+
* Server code reads `SLACK_EXTENDED_SCOPES`. Server-only vars never reach browser
159+
* bundles, so client evaluation reads the `NEXT_PUBLIC_SLACK_EXTENDED_SCOPES`
160+
* twin (see {@link isBillingEnabled}) — deployments must set both together. The
161+
* server value decides the grant; the client value only decides what the credential
162+
* pickers advertise and treat as missing.
163+
*/
164+
export const isSlackExtendedScopesEnabled =
165+
typeof window === 'undefined'
166+
? isTruthy(env.SLACK_EXTENDED_SCOPES)
167+
: isTruthy(getEnv('NEXT_PUBLIC_SLACK_EXTENDED_SCOPES'))
168+
147169
/**
148170
* Is Trigger.dev enabled for async job processing
149171
*/

apps/sim/lib/core/config/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ export const env = createEnv({
421421
SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID
422422
SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret
423423
SLACK_SIGNING_SECRET: z.string().optional(), // Official Sim Slack app signing secret (verifies inbound events for the native OAuth trigger)
424+
SLACK_EXTENDED_SCOPES: z.boolean().optional(), // Request app_mentions:read, assistant:write, im:history — only where the Slack app is approved for them
424425
REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID
425426
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
426427
WEBFLOW_CLIENT_ID: z.string().optional(), // Webflow OAuth client ID
@@ -571,6 +572,7 @@ export const env = createEnv({
571572
// Feature Flags
572573
NEXT_PUBLIC_SSO_ENABLED: z.boolean().optional(), // Enable SSO login UI components
573574
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control (permission groups) on self-hosted
575+
NEXT_PUBLIC_SLACK_EXTENDED_SCOPES: z.boolean().optional(), // Client twin of SLACK_EXTENDED_SCOPES — set both together
574576
NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED: z.boolean().optional(), // Enable custom blocks (deploy-as-block) settings on self-hosted
575577
NEXT_PUBLIC_WHITELABELING_ENABLED: z.boolean().optional(), // Enable whitelabeling on self-hosted (bypasses hosted requirements)
576578
NEXT_PUBLIC_AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements)
@@ -612,6 +614,7 @@ export const env = createEnv({
612614
NEXT_PUBLIC_BRAND_BACKGROUND_COLOR: process.env.NEXT_PUBLIC_BRAND_BACKGROUND_COLOR,
613615
NEXT_PUBLIC_SSO_ENABLED: process.env.NEXT_PUBLIC_SSO_ENABLED,
614616
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: process.env.NEXT_PUBLIC_ACCESS_CONTROL_ENABLED,
617+
NEXT_PUBLIC_SLACK_EXTENDED_SCOPES: process.env.NEXT_PUBLIC_SLACK_EXTENDED_SCOPES,
615618
NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED: process.env.NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED,
616619
NEXT_PUBLIC_WHITELABELING_ENABLED: process.env.NEXT_PUBLIC_WHITELABELING_ENABLED,
617620
NEXT_PUBLIC_AUDIT_LOGS_ENABLED: process.env.NEXT_PUBLIC_AUDIT_LOGS_ENABLED,

apps/sim/lib/oauth/oauth.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
ZoomIcon,
5858
} from '@/components/icons'
5959
import { env } from '@/lib/core/config/env'
60+
import { isSlackExtendedScopesEnabled } from '@/lib/core/config/env-flags'
6061
import {
6162
DEFAULT_MAX_ERROR_BODY_BYTES,
6263
readResponseTextWithLimit,
@@ -66,6 +67,16 @@ import type { OAuthProviderConfig } from './types'
6667

6768
const logger = createLogger('OAuth')
6869

70+
/**
71+
* Slack scopes requested only where the app is approved for them, gated by
72+
* {@link isSlackExtendedScopesEnabled}. Slack rejects the entire authorization
73+
* with "unapproved permissions requested" when any requested scope is not on the
74+
* app's approved list, so these stay out of the default grant.
75+
*/
76+
const SLACK_APPROVAL_GATED_SCOPES = isSlackExtendedScopesEnabled
77+
? (['assistant:write', 'app_mentions:read', 'im:history'] as const)
78+
: ([] as const)
79+
6980
export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
7081
'claude-platform': {
7182
name: 'Claude Platform',
@@ -782,9 +793,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
782793
'groups:write',
783794
'chat:write',
784795
'chat:write.public',
785-
'assistant:write',
786-
'app_mentions:read',
787-
'im:history',
796+
...SLACK_APPROVAL_GATED_SCOPES,
788797
'im:write',
789798
'im:read',
790799
'users:read',

packages/testing/src/mocks/env-flags.mock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface EnvFlagsMockState {
2121
isEmailPasswordEnabled: boolean
2222
isSignupMxValidationEnabled: boolean
2323
isAppConfigEnabled: boolean
24+
isSlackExtendedScopesEnabled: boolean
2425
isTriggerDevEnabled: boolean
2526
isSsoEnabled: boolean
2627
isAccessControlEnabled: boolean
@@ -61,6 +62,7 @@ const defaultEnvFlagsState: EnvFlagsMockState = {
6162
isEmailPasswordEnabled: true,
6263
isSignupMxValidationEnabled: false,
6364
isAppConfigEnabled: false,
65+
isSlackExtendedScopesEnabled: false,
6466
isTriggerDevEnabled: false,
6567
isSsoEnabled: false,
6668
isAccessControlEnabled: false,

0 commit comments

Comments
 (0)