Skip to content

Commit 02a8ad8

Browse files
committed
fix(deploy): keep public API exposure admin-only
Deploying moved to write, but enabling the public API is a different risk class: it makes the workflow callable with no authentication at all, exposing every credential and environment variable it references to anonymous callers. An editor can ship a version; only an admin can expose it to the internet. - workflows.public_api.update goes back to minimumRole 'admin' - the Access control in Edit API Info is disabled for non-admins, with a tooltip explaining why The whole ButtonGroup is gated rather than just the Public option: an already-public workflow would otherwise let an editor switch it back to API Key and hit a 403, which is the client/server disagreement this is meant to avoid. An editor still sees the current state, just cannot change it either way.
1 parent d24962a commit 02a8ad8

3 files changed

Lines changed: 42 additions & 10 deletions

File tree

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import {
1414
ChipModalHeader,
1515
Input,
1616
Label,
17+
Tooltip,
1718
} from '@sim/emcn'
1819
import { getErrorMessage } from '@sim/utils/errors'
1920
import { useParams } from 'next/navigation'
2021
import { getMeaningfulWorkflowDescription } from '@/lib/mcp/workflow-tool-schema'
2122
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
2223
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
2324
import type { InputFormatField } from '@/lib/workflows/types'
25+
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
2426
import { useDeploymentInfo, useUpdatePublicApi } from '@/hooks/queries/deployments'
2527
import { useUpdateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows'
2628
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -51,6 +53,12 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
5153
const { data: deploymentData } = useDeploymentInfo(workflowId, { enabled: open })
5254
const updatePublicApiMutation = useUpdatePublicApi()
5355
const { isPublicApiDisabled } = usePermissionConfig()
56+
/**
57+
* Deploying only needs `write`, but exposing a workflow to unauthenticated
58+
* callers stays admin-only — `workflowOperations.updatePublicApi` enforces
59+
* the same boundary server-side.
60+
*/
61+
const { canAdmin: canSetPublicAccess } = useUserPermissionsContext()
5462

5563
const [description, setDescription] = useState('')
5664
const [paramDescriptions, setParamDescriptions] = useState<Record<string, string>>({})
@@ -218,13 +226,23 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
218226

219227
{!isPublicApiDisabled && (
220228
<ChipModalField type='custom' title='Access'>
221-
<ButtonGroup
222-
value={accessMode}
223-
onValueChange={(val) => setAccessMode(val as 'api_key' | 'public')}
224-
>
225-
<ButtonGroupItem value='api_key'>API Key</ButtonGroupItem>
226-
<ButtonGroupItem value='public'>Public</ButtonGroupItem>
227-
</ButtonGroup>
229+
<Tooltip.Root>
230+
<Tooltip.Trigger asChild>
231+
<span className='inline-flex w-fit'>
232+
<ButtonGroup
233+
value={accessMode}
234+
onValueChange={(val) => setAccessMode(val as 'api_key' | 'public')}
235+
disabled={!canSetPublicAccess}
236+
>
237+
<ButtonGroupItem value='api_key'>API Key</ButtonGroupItem>
238+
<ButtonGroupItem value='public'>Public</ButtonGroupItem>
239+
</ButtonGroup>
240+
</span>
241+
</Tooltip.Trigger>
242+
{!canSetPublicAccess && (
243+
<Tooltip.Content>Only admins can change public API access</Tooltip.Content>
244+
)}
245+
</Tooltip.Root>
228246
<p className='mt-1 text-[var(--text-secondary)] text-caption'>
229247
{accessMode === 'public'
230248
? 'Anyone can call this API without authentication. You will be billed for all usage.'

apps/sim/lib/core/application/deployment-permission-matrix.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ describe('deployment permission matrix', () => {
2424
['undeploy', workflowOperations.undeploy],
2525
['deployChat', workflowOperations.deployChat],
2626
['undeployChat', workflowOperations.undeployChat],
27-
['updatePublicApi', workflowOperations.updatePublicApi],
2827
['activateVersion', workflowOperations.activateVersion],
2928
['revertVersion', workflowOperations.revertVersion],
3029
['updateVersion', workflowOperations.updateVersion],
@@ -43,14 +42,23 @@ describe('deployment permission matrix', () => {
4342
})
4443
})
4544

46-
describe('credential and code-injection surfaces stay admin', () => {
45+
describe('credential, exposure, and code-injection surfaces stay admin', () => {
4746
it('minting a workspace API key requires admin', () => {
4847
expect(apiKeyOperations.createFromCopilot.minimumRole).toBe('admin')
4948
})
5049

5150
it('workflow policy (lock) requires admin', () => {
5251
expect(workflowOperations.updatePolicy.minimumRole).toBe('admin')
5352
})
53+
54+
/**
55+
* Enabling the public API makes the workflow callable with no auth at all.
56+
* It stays admin even though deploying moved to write — an editor can ship
57+
* a version, but only an admin can expose it to anonymous callers.
58+
*/
59+
it('exposing a workflow as a public API requires admin', () => {
60+
expect(workflowOperations.updatePublicApi.minimumRole).toBe('admin')
61+
})
5462
})
5563

5664
/**

apps/sim/lib/workflows/application/operations.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,15 @@ export const workflowOperations = {
202202
workspaceApiKey: 'deny',
203203
...HUMAN_WORKFLOW_PRINCIPAL_POLICY,
204204
}),
205+
/**
206+
* Deliberately admin while the rest of the deployment lifecycle is write:
207+
* enabling this makes the workflow callable with no authentication at all,
208+
* exposing every credential and environment variable it references to
209+
* anonymous callers. That is a different risk class from shipping a version.
210+
*/
205211
updatePublicApi: defineWorkspaceOperation({
206212
id: 'workflows.public_api.update',
207-
minimumRole: 'write',
213+
minimumRole: 'admin',
208214
workspaceApiKey: 'deny',
209215
principalKinds: ['session'],
210216
}),

0 commit comments

Comments
 (0)