Skip to content

Commit 090aa07

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 090aa07

File tree

19 files changed

+12704
-37
lines changed

19 files changed

+12704
-37
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: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,39 @@ 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 { db: dbClient, workflow: workflowTable } = await import('@sim/db')
263+
const { eq } = await import('drizzle-orm')
264+
const [wf] = await dbClient
265+
.select({ isPublicApi: workflowTable.isPublicApi, userId: workflowTable.userId })
266+
.from(workflowTable)
267+
.where(eq(workflowTable.id, workflowId))
268+
.limit(1)
269+
270+
if (!wf?.isPublicApi) {
271+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
272+
}
273+
274+
const { isPublicApiDisabled } = await import('@/lib/core/config/feature-flags')
275+
if (isPublicApiDisabled) {
276+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
277+
}
278+
279+
const { getUserPermissionConfig } = await import('@/ee/access-control/utils/permission-check')
280+
const ownerConfig = await getUserPermissionConfig(wf.userId)
281+
if (ownerConfig?.disablePublicApi) {
282+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
283+
}
284+
285+
userId = wf.userId
286+
isPublicApiAccess = true
287+
} else {
288+
userId = auth.userId
259289
}
260-
const userId = auth.userId
261290

262291
let body: any = {}
263292
try {
@@ -284,7 +313,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
284313
)
285314
}
286315

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

289318
const {
290319
selectedOutputs,
@@ -341,7 +370,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
341370
// For API key and internal JWT auth, the entire body is the input (except for our control fields)
342371
// For session auth, the input is explicitly provided in the input field
343372
const input =
344-
auth.authType === 'api_key' || auth.authType === 'internal_jwt'
373+
isPublicApiAccess || auth.authType === 'api_key' || auth.authType === 'internal_jwt'
345374
? (() => {
346375
const {
347376
selectedOutputs,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface WorkflowDeploymentInfo {
2121
endpoint: string
2222
exampleCommand: string
2323
needsRedeployment: boolean
24+
isPublicApi?: boolean
2425
}
2526

2627
interface ApiDeployProps {
@@ -107,12 +108,12 @@ export function ApiDeploy({
107108
if (!info) return ''
108109
const endpoint = getBaseEndpoint()
109110
const payload = getPayloadObject()
111+
const isPublic = info.isPublicApi
110112

111113
switch (language) {
112114
case 'curl':
113115
return `curl -X POST \\
114-
-H "X-API-Key: $SIM_API_KEY" \\
115-
-H "Content-Type: application/json" \\
116+
${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\
116117
-d '${JSON.stringify(payload)}' \\
117118
${endpoint}`
118119

@@ -123,8 +124,7 @@ import requests
123124
response = requests.post(
124125
"${endpoint}",
125126
headers={
126-
"X-API-Key": os.environ.get("SIM_API_KEY"),
127-
"Content-Type": "application/json"
127+
${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json"
128128
},
129129
json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}
130130
)
@@ -135,8 +135,7 @@ print(response.json())`
135135
return `const response = await fetch("${endpoint}", {
136136
method: "POST",
137137
headers: {
138-
"X-API-Key": process.env.SIM_API_KEY,
139-
"Content-Type": "application/json"
138+
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json"
140139
},
141140
body: JSON.stringify(${JSON.stringify(payload)})
142141
});
@@ -148,8 +147,7 @@ console.log(data);`
148147
return `const response = await fetch("${endpoint}", {
149148
method: "POST",
150149
headers: {
151-
"X-API-Key": process.env.SIM_API_KEY,
152-
"Content-Type": "application/json"
150+
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json"
153151
},
154152
body: JSON.stringify(${JSON.stringify(payload)})
155153
});
@@ -166,12 +164,12 @@ console.log(data);`
166164
if (!info) return ''
167165
const endpoint = getBaseEndpoint()
168166
const payload = getStreamPayloadObject()
167+
const isPublic = info.isPublicApi
169168

170169
switch (language) {
171170
case 'curl':
172171
return `curl -X POST \\
173-
-H "X-API-Key: $SIM_API_KEY" \\
174-
-H "Content-Type: application/json" \\
172+
${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\
175173
-d '${JSON.stringify(payload)}' \\
176174
${endpoint}`
177175

@@ -182,8 +180,7 @@ import requests
182180
response = requests.post(
183181
"${endpoint}",
184182
headers={
185-
"X-API-Key": os.environ.get("SIM_API_KEY"),
186-
"Content-Type": "application/json"
183+
${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json"
187184
},
188185
json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')},
189186
stream=True
@@ -197,8 +194,7 @@ for line in response.iter_lines():
197194
return `const response = await fetch("${endpoint}", {
198195
method: "POST",
199196
headers: {
200-
"X-API-Key": process.env.SIM_API_KEY,
201-
"Content-Type": "application/json"
197+
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json"
202198
},
203199
body: JSON.stringify(${JSON.stringify(payload)})
204200
});
@@ -216,8 +212,7 @@ while (true) {
216212
return `const response = await fetch("${endpoint}", {
217213
method: "POST",
218214
headers: {
219-
"X-API-Key": process.env.SIM_API_KEY,
220-
"Content-Type": "application/json"
215+
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json"
221216
},
222217
body: JSON.stringify(${JSON.stringify(payload)})
223218
});
@@ -241,14 +236,14 @@ while (true) {
241236
const endpoint = getBaseEndpoint()
242237
const baseUrl = endpoint.split('/api/workflows/')[0]
243238
const payload = getPayloadObject()
239+
const isPublic = info.isPublicApi
244240

245241
switch (asyncExampleType) {
246242
case 'execute':
247243
switch (language) {
248244
case 'curl':
249245
return `curl -X POST \\
250-
-H "X-API-Key: $SIM_API_KEY" \\
251-
-H "Content-Type: application/json" \\
246+
${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\
252247
-H "X-Execution-Mode: async" \\
253248
-d '${JSON.stringify(payload)}' \\
254249
${endpoint}`
@@ -260,8 +255,7 @@ import requests
260255
response = requests.post(
261256
"${endpoint}",
262257
headers={
263-
"X-API-Key": os.environ.get("SIM_API_KEY"),
264-
"Content-Type": "application/json",
258+
${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json",
265259
"X-Execution-Mode": "async"
266260
},
267261
json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}
@@ -274,8 +268,7 @@ print(job) # Contains jobId and executionId`
274268
return `const response = await fetch("${endpoint}", {
275269
method: "POST",
276270
headers: {
277-
"X-API-Key": process.env.SIM_API_KEY,
278-
"Content-Type": "application/json",
271+
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json",
279272
"X-Execution-Mode": "async"
280273
},
281274
body: JSON.stringify(${JSON.stringify(payload)})
@@ -288,8 +281,7 @@ console.log(job); // Contains jobId and executionId`
288281
return `const response = await fetch("${endpoint}", {
289282
method: "POST",
290283
headers: {
291-
"X-API-Key": process.env.SIM_API_KEY,
292-
"Content-Type": "application/json",
284+
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json",
293285
"X-Execution-Mode": "async"
294286
},
295287
body: JSON.stringify(${JSON.stringify(payload)})

0 commit comments

Comments
 (0)