Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(public-api): add env var and permission group controls to disable public API access#3317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -145,4 +145,4 @@ | ||
| "zep", | ||
| "zoom" | ||
| ] | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -254,10 +254,49 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: | ||
| try { | ||
| const auth = await checkHybridAuth(req, { requireWorkflowId: false }) | ||
| let userId: string | ||
| let isPublicApiAccess = false | ||
| if (!auth.success || !auth.userId) { | ||
| return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) | ||
| const hasExplicitCredentials = | ||
| req.headers.has('x-api-key') || req.headers.get('authorization')?.startsWith('Bearer ') | ||
| if (hasExplicitCredentials) { | ||
| return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) | ||
| } | ||
| const { db: dbClient, workflow: workflowTable } = await import('@sim/db') | ||
| const { eq } = await import('drizzle-orm') | ||
| const [wf] = await dbClient | ||
| .select({ | ||
| isPublicApi: workflowTable.isPublicApi, | ||
| isDeployed: workflowTable.isDeployed, | ||
| userId: workflowTable.userId, | ||
| }) | ||
| .from(workflowTable) | ||
| .where(eq(workflowTable.id, workflowId)) | ||
| .limit(1) | ||
| if (!wf?.isPublicApi || !wf.isDeployed) { | ||
| return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) | ||
| } | ||
| const { isPublicApiDisabled } = await import('@/lib/core/config/feature-flags') | ||
| if (isPublicApiDisabled) { | ||
| return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) | ||
| } | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const { getUserPermissionConfig } = await import('@/ee/access-control/utils/permission-check') | ||
| const ownerConfig = await getUserPermissionConfig(wf.userId) | ||
| if (ownerConfig?.disablePublicApi) { | ||
| return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) | ||
| } | ||
| userId = wf.userId | ||
| isPublicApiAccess = true | ||
| } else { | ||
| userId = auth.userId | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| const userId = auth.userId | ||
| let body: any = {} | ||
| try { | ||
| @@ -284,7 +323,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: | ||
| ) | ||
| } | ||
| const defaultTriggerType = auth.authType === 'api_key' ? 'api' : 'manual' | ||
| const defaultTriggerType = isPublicApiAccess || auth.authType === 'api_key' ? 'api' : 'manual' | ||
| const { | ||
| selectedOutputs, | ||
| @@ -305,7 +344,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: | ||
| | { startBlockId: string; sourceSnapshot: SerializableExecutionState } | ||
| | undefined | ||
| if (rawRunFromBlock) { | ||
| if (rawRunFromBlock.sourceSnapshot) { | ||
| if (rawRunFromBlock.sourceSnapshot && !isPublicApiAccess) { | ||
| // Public API callers cannot inject arbitrary block state via sourceSnapshot. | ||
| // They must use executionId to resume from a server-stored execution state. | ||
| resolvedRunFromBlock = { | ||
| startBlockId: rawRunFromBlock.startBlockId, | ||
| sourceSnapshot: rawRunFromBlock.sourceSnapshot as SerializableExecutionState, | ||
| @@ -341,7 +382,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: | ||
| // For API key and internal JWT auth, the entire body is the input (except for our control fields) | ||
| // For session auth, the input is explicitly provided in the input field | ||
| const input = | ||
| auth.authType === 'api_key' || auth.authType === 'internal_jwt' | ||
| isPublicApiAccess || auth.authType === 'api_key' || auth.authType === 'internal_jwt' | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ? (() => { | ||
| const { | ||
| selectedOutputs, | ||
| @@ -360,7 +401,14 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: | ||
| })() | ||
| : validatedInput | ||
| const shouldUseDraftState = useDraftState ?? auth.authType === 'session' | ||
| // Public API callers must not inject arbitrary workflow state overrides (code injection risk). | ||
| // stopAfterBlockId and runFromBlock are safe — they control execution flow within the deployed state. | ||
| const sanitizedWorkflowStateOverride = isPublicApiAccess ? undefined : workflowStateOverride | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // Public API callers always execute the deployed state, never the draft. | ||
| const shouldUseDraftState = isPublicApiAccess | ||
| ? false | ||
| : (useDraftState ?? auth.authType === 'session') | ||
| const workflowAuthorization = await authorizeWorkflowByWorkspacePermission({ | ||
| workflowId, | ||
| userId, | ||
| @@ -533,7 +581,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: | ||
| ) | ||
| } | ||
| const effectiveWorkflowStateOverride = workflowStateOverride || cachedWorkflowData || undefined | ||
| const effectiveWorkflowStateOverride = | ||
| sanitizedWorkflowStateOverride || cachedWorkflowData || undefined | ||
| if (!enableSSE) { | ||
| logger.info(`[${requestId}] Using non-SSE execution (direct JSON response)`) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -21,6 +21,7 @@ interface WorkflowDeploymentInfo { | ||
| endpoint: string | ||
| exampleCommand: string | ||
| needsRedeployment: boolean | ||
| isPublicApi?: boolean | ||
| } | ||
| interface ApiDeployProps { | ||
| @@ -107,12 +108,12 @@ export function ApiDeploy({ | ||
| if (!info) return '' | ||
| const endpoint = getBaseEndpoint() | ||
| const payload = getPayloadObject() | ||
| const isPublic = info.isPublicApi | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| switch (language) { | ||
| case 'curl': | ||
| return `curl -X POST \\ | ||
| -H "X-API-Key: $SIM_API_KEY" \\ | ||
| -H "Content-Type: application/json" \\ | ||
| ${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\ | ||
| -d '${JSON.stringify(payload)}' \\ | ||
| ${endpoint}` | ||
| @@ -123,8 +124,7 @@ import requests | ||
| response = requests.post( | ||
| "${endpoint}", | ||
| headers={ | ||
| "X-API-Key": os.environ.get("SIM_API_KEY"), | ||
| "Content-Type": "application/json" | ||
| ${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json" | ||
| }, | ||
| json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')} | ||
| ) | ||
| @@ -135,8 +135,7 @@ print(response.json())` | ||
| return `const response = await fetch("${endpoint}", { | ||
| method: "POST", | ||
| headers: { | ||
| "X-API-Key": process.env.SIM_API_KEY, | ||
| "Content-Type": "application/json" | ||
| ${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify(${JSON.stringify(payload)}) | ||
| }); | ||
| @@ -148,8 +147,7 @@ console.log(data);` | ||
| return `const response = await fetch("${endpoint}", { | ||
| method: "POST", | ||
| headers: { | ||
| "X-API-Key": process.env.SIM_API_KEY, | ||
| "Content-Type": "application/json" | ||
| ${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify(${JSON.stringify(payload)}) | ||
| }); | ||
| @@ -166,12 +164,12 @@ console.log(data);` | ||
| if (!info) return '' | ||
| const endpoint = getBaseEndpoint() | ||
| const payload = getStreamPayloadObject() | ||
| const isPublic = info.isPublicApi | ||
| switch (language) { | ||
| case 'curl': | ||
| return `curl -X POST \\ | ||
| -H "X-API-Key: $SIM_API_KEY" \\ | ||
| -H "Content-Type: application/json" \\ | ||
| ${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\ | ||
| -d '${JSON.stringify(payload)}' \\ | ||
| ${endpoint}` | ||
| @@ -182,8 +180,7 @@ import requests | ||
| response = requests.post( | ||
| "${endpoint}", | ||
| headers={ | ||
| "X-API-Key": os.environ.get("SIM_API_KEY"), | ||
| "Content-Type": "application/json" | ||
| ${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json" | ||
| }, | ||
| json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}, | ||
| stream=True | ||
| @@ -197,8 +194,7 @@ for line in response.iter_lines(): | ||
| return `const response = await fetch("${endpoint}", { | ||
| method: "POST", | ||
| headers: { | ||
| "X-API-Key": process.env.SIM_API_KEY, | ||
| "Content-Type": "application/json" | ||
| ${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify(${JSON.stringify(payload)}) | ||
| }); | ||
| @@ -216,8 +212,7 @@ while (true) { | ||
| return `const response = await fetch("${endpoint}", { | ||
| method: "POST", | ||
| headers: { | ||
| "X-API-Key": process.env.SIM_API_KEY, | ||
| "Content-Type": "application/json" | ||
| ${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify(${JSON.stringify(payload)}) | ||
| }); | ||
| @@ -241,14 +236,14 @@ while (true) { | ||
| const endpoint = getBaseEndpoint() | ||
| const baseUrl = endpoint.split('/api/workflows/')[0] | ||
| const payload = getPayloadObject() | ||
| const isPublic = info.isPublicApi | ||
| switch (asyncExampleType) { | ||
| case 'execute': | ||
| switch (language) { | ||
| case 'curl': | ||
| return `curl -X POST \\ | ||
| -H "X-API-Key: $SIM_API_KEY" \\ | ||
| -H "Content-Type: application/json" \\ | ||
| ${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\ | ||
| -H "X-Execution-Mode: async" \\ | ||
| -d '${JSON.stringify(payload)}' \\ | ||
| ${endpoint}` | ||
| @@ -260,8 +255,7 @@ import requests | ||
| response = requests.post( | ||
| "${endpoint}", | ||
| headers={ | ||
| "X-API-Key": os.environ.get("SIM_API_KEY"), | ||
| "Content-Type": "application/json", | ||
| ${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json", | ||
| "X-Execution-Mode": "async" | ||
| }, | ||
| json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')} | ||
| @@ -274,8 +268,7 @@ print(job) # Contains jobId and executionId` | ||
| return `const response = await fetch("${endpoint}", { | ||
| method: "POST", | ||
| headers: { | ||
| "X-API-Key": process.env.SIM_API_KEY, | ||
| "Content-Type": "application/json", | ||
| ${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json", | ||
| "X-Execution-Mode": "async" | ||
| }, | ||
| body: JSON.stringify(${JSON.stringify(payload)}) | ||
| @@ -288,8 +281,7 @@ console.log(job); // Contains jobId and executionId` | ||
| return `const response = await fetch("${endpoint}", { | ||
| method: "POST", | ||
| headers: { | ||
| "X-API-Key": process.env.SIM_API_KEY, | ||
| "Content-Type": "application/json", | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json", | ||
| "X-Execution-Mode": "async" | ||
| }, | ||
| body: JSON.stringify(${JSON.stringify(payload)}) | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.