Skip to content

Commit a487c88

Browse files
waleedlatif1claude
andcommitted
feat(public-api): add env var and permission group controls to disable public API access
Add DISABLE_PUBLIC_API / NEXT_PUBLIC_DISABLE_PUBLIC_API environment variables and disablePublicApi permission group config option to allow self-hosted deployments and enterprise admins to globally disable the public API toggle. When disabled: the Access toggle is hidden in the Edit API Info modal, the execute route blocks unauthenticated public access (401), and the public-api PATCH route rejects enabling public API (403). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fe34d23 commit a487c88

File tree

19 files changed

+12747
-42
lines changed

19 files changed

+12747
-42
lines changed

apps/docs/components/ui/icon-mapping.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ import {
3737
EyeIcon,
3838
FirecrawlIcon,
3939
FirefliesIcon,
40-
GitLabIcon,
4140
GithubIcon,
41+
GitLabIcon,
4242
GmailIcon,
4343
GongIcon,
4444
GoogleBooksIcon,
@@ -71,9 +71,9 @@ import {
7171
LinearIcon,
7272
LinkedInIcon,
7373
LinkupIcon,
74-
MailServerIcon,
7574
MailchimpIcon,
7675
MailgunIcon,
76+
MailServerIcon,
7777
Mem0Icon,
7878
MicrosoftDataverseIcon,
7979
MicrosoftExcelIcon,
@@ -106,8 +106,6 @@ import {
106106
ResendIcon,
107107
RevenueCatIcon,
108108
S3Icon,
109-
SQSIcon,
110-
STTIcon,
111109
SalesforceIcon,
112110
SearchIcon,
113111
SendgridIcon,
@@ -119,17 +117,19 @@ import {
119117
SimilarwebIcon,
120118
SlackIcon,
121119
SmtpIcon,
120+
SQSIcon,
122121
SshIcon,
122+
STTIcon,
123123
StagehandIcon,
124124
StripeIcon,
125125
SupabaseIcon,
126-
TTSIcon,
127126
TavilyIcon,
128127
TelegramIcon,
129128
TextractIcon,
130129
TinybirdIcon,
131130
TranslateIcon,
132131
TrelloIcon,
132+
TTSIcon,
133133
TwilioIcon,
134134
TypeformIcon,
135135
UpstashIcon,
@@ -140,11 +140,11 @@ import {
140140
WhatsAppIcon,
141141
WikipediaIcon,
142142
WordpressIcon,
143+
xIcon,
143144
YouTubeIcon,
144145
ZendeskIcon,
145146
ZepIcon,
146147
ZoomIcon,
147-
xIcon,
148148
} from '@/components/icons'
149149

150150
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>

apps/docs/content/docs/en/tools/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,4 @@
145145
"zep",
146146
"zoom"
147147
]
148-
}
148+
}

apps/sim/app/api/workflows/[id]/deploy/route.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
5151
deployedAt: null,
5252
apiKey: null,
5353
needsRedeployment: false,
54+
isPublicApi: workflowData.isPublicApi ?? false,
5455
})
5556
}
5657

@@ -98,6 +99,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
9899
isDeployed: workflowData.isDeployed,
99100
deployedAt: workflowData.deployedAt,
100101
needsRedeployment,
102+
isPublicApi: workflowData.isPublicApi ?? false,
101103
})
102104
} catch (error: any) {
103105
logger.error(`[${requestId}] Error fetching deployment info: ${id}`, error)
@@ -301,6 +303,49 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
301303
}
302304
}
303305

306+
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
307+
const requestId = generateRequestId()
308+
const { id } = await params
309+
310+
try {
311+
const { error, session } = await validateWorkflowPermissions(id, requestId, 'admin')
312+
if (error) {
313+
return createErrorResponse(error.message, error.status)
314+
}
315+
316+
const body = await request.json()
317+
const { isPublicApi } = body
318+
319+
if (typeof isPublicApi !== 'boolean') {
320+
return createErrorResponse('Invalid request body: isPublicApi must be a boolean', 400)
321+
}
322+
323+
if (isPublicApi) {
324+
const { validatePublicApiAllowed, PublicApiNotAllowedError } = await import(
325+
'@/ee/access-control/utils/permission-check'
326+
)
327+
try {
328+
await validatePublicApiAllowed(session?.user?.id)
329+
} catch (err) {
330+
if (err instanceof PublicApiNotAllowedError) {
331+
return createErrorResponse('Public API access is disabled', 403)
332+
}
333+
throw err
334+
}
335+
}
336+
337+
await db.update(workflow).set({ isPublicApi }).where(eq(workflow.id, id))
338+
339+
logger.info(`[${requestId}] Updated isPublicApi for workflow ${id} to ${isPublicApi}`)
340+
341+
return createSuccessResponse({ isPublicApi })
342+
} catch (error: unknown) {
343+
const message = error instanceof Error ? error.message : 'Failed to update deployment settings'
344+
logger.error(`[${requestId}] Error updating deployment settings: ${id}`, { error })
345+
return createErrorResponse(message, 500)
346+
}
347+
}
348+
304349
export async function DELETE(
305350
request: NextRequest,
306351
{ params }: { params: Promise<{ id: string }> }

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,49 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
254254

255255
try {
256256
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
257+
258+
let userId: string
259+
let isPublicApiAccess = false
260+
257261
if (!auth.success || !auth.userId) {
258-
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
262+
const hasExplicitCredentials =
263+
req.headers.has('x-api-key') || req.headers.get('authorization')?.startsWith('Bearer ')
264+
if (hasExplicitCredentials) {
265+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
266+
}
267+
268+
const { db: dbClient, workflow: workflowTable } = await import('@sim/db')
269+
const { eq } = await import('drizzle-orm')
270+
const [wf] = await dbClient
271+
.select({
272+
isPublicApi: workflowTable.isPublicApi,
273+
isDeployed: workflowTable.isDeployed,
274+
userId: workflowTable.userId,
275+
})
276+
.from(workflowTable)
277+
.where(eq(workflowTable.id, workflowId))
278+
.limit(1)
279+
280+
if (!wf?.isPublicApi || !wf.isDeployed) {
281+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
282+
}
283+
284+
const { isPublicApiDisabled } = await import('@/lib/core/config/feature-flags')
285+
if (isPublicApiDisabled) {
286+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
287+
}
288+
289+
const { getUserPermissionConfig } = await import('@/ee/access-control/utils/permission-check')
290+
const ownerConfig = await getUserPermissionConfig(wf.userId)
291+
if (ownerConfig?.disablePublicApi) {
292+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
293+
}
294+
295+
userId = wf.userId
296+
isPublicApiAccess = true
297+
} else {
298+
userId = auth.userId
259299
}
260-
const userId = auth.userId
261300

262301
let body: any = {}
263302
try {
@@ -284,7 +323,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
284323
)
285324
}
286325

287-
const defaultTriggerType = auth.authType === 'api_key' ? 'api' : 'manual'
326+
const defaultTriggerType = isPublicApiAccess || auth.authType === 'api_key' ? 'api' : 'manual'
288327

289328
const {
290329
selectedOutputs,
@@ -341,7 +380,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
341380
// For API key and internal JWT auth, the entire body is the input (except for our control fields)
342381
// For session auth, the input is explicitly provided in the input field
343382
const input =
344-
auth.authType === 'api_key' || auth.authType === 'internal_jwt'
383+
isPublicApiAccess || auth.authType === 'api_key' || auth.authType === 'internal_jwt'
345384
? (() => {
346385
const {
347386
selectedOutputs,
@@ -360,7 +399,16 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
360399
})()
361400
: validatedInput
362401

363-
const shouldUseDraftState = useDraftState ?? auth.authType === 'session'
402+
// Public API callers must not override workflow state, stop execution early, or resume from a block.
403+
// These are administrative/debugging features restricted to authenticated users.
404+
const sanitizedWorkflowStateOverride = isPublicApiAccess ? undefined : workflowStateOverride
405+
const sanitizedStopAfterBlockId = isPublicApiAccess ? undefined : stopAfterBlockId
406+
const sanitizedRunFromBlock = isPublicApiAccess ? undefined : resolvedRunFromBlock
407+
408+
// Public API callers always execute the deployed state, never the draft.
409+
const shouldUseDraftState = isPublicApiAccess
410+
? false
411+
: (useDraftState ?? auth.authType === 'session')
364412
const workflowAuthorization = await authorizeWorkflowByWorkspacePermission({
365413
workflowId,
366414
userId,
@@ -533,7 +581,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
533581
)
534582
}
535583

536-
const effectiveWorkflowStateOverride = workflowStateOverride || cachedWorkflowData || undefined
584+
const effectiveWorkflowStateOverride =
585+
sanitizedWorkflowStateOverride || cachedWorkflowData || undefined
537586

538587
if (!enableSSE) {
539588
logger.info(`[${requestId}] Using non-SSE execution (direct JSON response)`)
@@ -575,8 +624,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
575624
loggingSession,
576625
includeFileBase64,
577626
base64MaxBytes,
578-
stopAfterBlockId,
579-
runFromBlock: resolvedRunFromBlock,
627+
stopAfterBlockId: sanitizedStopAfterBlockId,
628+
runFromBlock: sanitizedRunFromBlock,
580629
abortSignal: timeoutController.signal,
581630
})
582631

@@ -973,8 +1022,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
9731022
abortSignal: timeoutController.signal,
9741023
includeFileBase64,
9751024
base64MaxBytes,
976-
stopAfterBlockId,
977-
runFromBlock: resolvedRunFromBlock,
1025+
stopAfterBlockId: sanitizedStopAfterBlockId,
1026+
runFromBlock: sanitizedRunFromBlock,
9781027
})
9791028

9801029
if (result.status === 'paused') {

0 commit comments

Comments
 (0)