Skip to content

Commit 43e2364

Browse files
feat(workflows): preserve execution principals
1 parent 85902eb commit 43e2364

68 files changed

Lines changed: 2364 additions & 139 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
2-
import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
2+
import {
3+
resolvePrincipalSubject,
4+
type WorkflowExecutionDelegatedPrincipal,
5+
} from '@sim/auth/principal'
36
import { createLogger } from '@sim/logger'
47
import { getErrorMessage } from '@sim/utils/errors'
58
import { type NextRequest, NextResponse } from 'next/server'
@@ -206,16 +209,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
206209
request,
207210
})
208211

209-
captureServerEvent(
210-
managedOAuthPrincipal.subjectUserId,
211-
'credential_used',
212-
{
213-
credential_type: 'managed_oauth',
214-
provider_id: toolMetadata.oauth.provider,
215-
workspace_id: managedOAuthPrincipal.workspaceId,
216-
},
217-
{ groups: { workspace: managedOAuthPrincipal.workspaceId } }
218-
)
212+
const managedOAuthSubject = resolvePrincipalSubject(managedOAuthPrincipal)
213+
if (managedOAuthSubject?.kind === 'sim_user') {
214+
captureServerEvent(
215+
managedOAuthSubject.userId,
216+
'credential_used',
217+
{
218+
credential_type: 'managed_oauth',
219+
provider_id: toolMetadata.oauth.provider,
220+
workspace_id: managedOAuthPrincipal.workspaceId,
221+
},
222+
{ groups: { workspace: managedOAuthPrincipal.workspaceId } }
223+
)
224+
}
219225

220226
return NextResponse.json(
221227
{

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ export const POST = withRouteHandler(
310310
resolvedActorUserId,
311311
{
312312
enabled: true,
313+
principal: {
314+
kind: 'system',
315+
serviceId: 'chat',
316+
workspaceId,
317+
workflowId: deployment.workflowId,
318+
},
313319
selectedOutputs,
314320
isSecureMode: true,
315321
workflowTriggerType: 'chat',

apps/sim/app/api/files/uploads/purposes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ async function principalUserId(principal: Principal, workspaceId?: string): Prom
251251
}
252252
case 'delegated':
253253
throw new UploadSessionError('forbidden', 'Delegated principals cannot create uploads')
254+
case 'system':
255+
throw new UploadSessionError('forbidden', 'System principals cannot create uploads')
254256
case 'credential_group_enrollment':
255257
throw new UploadSessionError(
256258
'forbidden',

apps/sim/app/api/mcp/serve/[serverId]/route.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ function createResolvedSecretTraceProvenance(userId: string, workspaceId = 'ws-1
7070
}
7171
}
7272

73+
const SESSION_PRINCIPAL = {
74+
kind: 'session',
75+
userId: 'user-1',
76+
sessionId: 'session-1',
77+
} as const
78+
79+
const PERSONAL_API_KEY_PRINCIPAL = {
80+
kind: 'personal_api_key',
81+
userId: 'user-1',
82+
keyId: 'personal-key-1',
83+
} as const
84+
85+
const WORKSPACE_API_KEY_PRINCIPAL = {
86+
kind: 'workspace_api_key',
87+
workspaceId: 'ws-1',
88+
keyId: 'workspace-key-1',
89+
} as const
90+
7391
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
7492

7593
vi.mock('@/lib/auth/internal', () => ({
@@ -215,6 +233,7 @@ describe('MCP Serve Route', () => {
215233
success: true,
216234
userId: 'user-1',
217235
authType: 'session',
236+
principal: SESSION_PRINCIPAL,
218237
})
219238
mockGetUserEntityPermissions.mockResolvedValueOnce('read')
220239

@@ -273,6 +292,7 @@ describe('MCP Serve Route', () => {
273292
userId: 'user-1',
274293
authType: 'api_key',
275294
apiKeyType: 'personal',
295+
principal: PERSONAL_API_KEY_PRINCIPAL,
276296
})
277297
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
278298
mockExecuteWorkflowService.mockResolvedValueOnce({
@@ -334,6 +354,7 @@ describe('MCP Serve Route', () => {
334354
userId: 'user-1',
335355
authType: 'api_key',
336356
apiKeyType: 'personal',
357+
principal: PERSONAL_API_KEY_PRINCIPAL,
337358
})
338359
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
339360

@@ -375,6 +396,7 @@ describe('MCP Serve Route', () => {
375396
authType: 'api_key',
376397
apiKeyType: 'workspace',
377398
workspaceId: 'ws-1',
399+
principal: WORKSPACE_API_KEY_PRINCIPAL,
378400
})
379401
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
380402
mockExecuteWorkflowService.mockResolvedValueOnce({
@@ -477,6 +499,7 @@ describe('MCP Serve Route', () => {
477499
success: true,
478500
userId: 'user-1',
479501
authType: 'session',
502+
principal: SESSION_PRINCIPAL,
480503
})
481504
mockGetUserEntityPermissions.mockResolvedValueOnce('read')
482505
mockExecuteWorkflowService.mockResolvedValueOnce({
@@ -1096,6 +1119,7 @@ describe('MCP Serve Route', () => {
10961119
userId: 'user-1',
10971120
authType: 'api_key',
10981121
apiKeyType: 'personal',
1122+
principal: PERSONAL_API_KEY_PRINCIPAL,
10991123
})
11001124
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
11011125
mockExecuteWorkflowService.mockResolvedValueOnce({
@@ -1193,6 +1217,7 @@ describe('MCP Serve Route', () => {
11931217
authType: 'api_key',
11941218
apiKeyType: 'workspace',
11951219
workspaceId: 'ws-1',
1220+
principal: WORKSPACE_API_KEY_PRINCIPAL,
11961221
})
11971222
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
11981223
mockExecuteWorkflowService.mockResolvedValueOnce({

apps/sim/app/api/mcp/serve/[serverId]/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
SUPPORTED_PROTOCOL_VERSIONS,
1818
type Tool,
1919
} from '@modelcontextprotocol/sdk/types.js'
20+
import type { WorkflowExecutionPrincipal } from '@sim/auth/principal'
2021
import { db } from '@sim/db'
2122
import {
2223
workflow,
@@ -96,6 +97,7 @@ interface RouteParams {
9697
interface ExecuteAuthContext {
9798
userId: string
9899
useAuthenticatedUserAsActor: boolean
100+
principal: WorkflowExecutionPrincipal
99101
}
100102

101103
function createResponse(id: RequestId, result: unknown): JSONRPCResultResponse {
@@ -364,6 +366,9 @@ async function authorizeMcpServeRequest(
364366
if (!auth.success || !auth.userId) {
365367
return { response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }
366368
}
369+
if (!auth.principal) {
370+
throw new Error('Authenticated MCP request is missing its principal')
371+
}
367372

368373
if (server.isPublic) return {}
369374

@@ -396,6 +401,7 @@ async function authorizeMcpServeRequest(
396401
executeAuthContext: {
397402
userId: auth.userId,
398403
useAuthenticatedUserAsActor: isPersonalApiKey,
404+
principal: auth.principal,
399405
},
400406
}
401407
}
@@ -856,6 +862,14 @@ async function handleToolsCall(
856862
*/
857863
const serviceResult = await executeWorkflowService({
858864
workflowId: tool.workflowId,
865+
principal:
866+
executeAuthContext?.principal ??
867+
({
868+
kind: 'system',
869+
serviceId: 'public_api',
870+
workspaceId: wf.workspaceId,
871+
workflowId: tool.workflowId,
872+
} satisfies WorkflowExecutionPrincipal),
859873
userId: actorUserId,
860874
input: workflowInput,
861875
triggerType: 'mcp',

apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ function createPausedExecution(overrides: PausedExecutionOverrides = {}) {
119119
executionId: overrides.snapshotExecutionId ?? EXECUTION_ID,
120120
workspaceId: overrides.snapshotWorkspaceId ?? WORKSPACE_ID,
121121
userId: overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID,
122+
principal: {
123+
version: 1,
124+
principal: {
125+
kind: 'session',
126+
userId: overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID,
127+
sessionId: 'session-original',
128+
},
129+
},
122130
billingAttribution,
123131
triggerType: 'manual',
124132
useDraftState: false,

apps/sim/app/api/tools/windchill/route.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
1+
import {
2+
type BoundWorkflowExecutionDelegatedPrincipal,
3+
requirePrincipalSubjectUserId,
4+
} from '@sim/auth/principal'
25
import { createLogger } from '@sim/logger'
36
import { getErrorMessage } from '@sim/utils/errors'
47
import { type NextRequest, NextResponse } from 'next/server'
@@ -53,16 +56,16 @@ const windchillSessionOrExecutorAuth = createInternalSessionOrExecutorAuth({
5356

5457
async function authenticateWindchillExecutor(
5558
request: NextRequest
56-
): Promise<WorkflowExecutionDelegatedPrincipal> {
59+
): Promise<BoundWorkflowExecutionDelegatedPrincipal> {
5760
const principal = await windchillSessionOrExecutorAuth.authenticate(request, {})
5861
if (
5962
principal.kind !== 'delegated' ||
6063
principal.serviceId !== 'executor' ||
61-
!('delegationContext' in principal)
64+
!principal.delegationContext
6265
) {
6366
throw new InternalUnauthenticatedError('Authentication required')
6467
}
65-
return principal
68+
return { ...principal, delegationContext: principal.delegationContext }
6669
}
6770

6871
type WindchillRouteOutput = Extract<WindchillOperationResponse, { success: true }>['output']
@@ -485,7 +488,7 @@ async function storeDownloadedFile({
485488
fileName,
486489
contentType,
487490
}: {
488-
principal: WorkflowExecutionDelegatedPrincipal
491+
principal: BoundWorkflowExecutionDelegatedPrincipal
489492
buffer: Buffer
490493
fileName: string
491494
contentType: string
@@ -501,14 +504,14 @@ async function storeDownloadedFile({
501504
buffer,
502505
fileName,
503506
contentType,
504-
principal.subjectUserId
507+
requirePrincipalSubjectUserId(principal)
505508
)
506509
}
507510
return uploadCopilotFile({
508511
buffer,
509512
fileName,
510513
contentType,
511-
userId: principal.subjectUserId,
514+
userId: requirePrincipalSubjectUserId(principal),
512515
})
513516
}
514517

@@ -518,7 +521,7 @@ async function executeDownload(
518521
| { operation: 'windchill_download_primary_content' }
519522
| { operation: 'windchill_download_attachment' }
520523
>,
521-
principal: WorkflowExecutionDelegatedPrincipal,
524+
principal: BoundWorkflowExecutionDelegatedPrincipal,
522525
signal: AbortSignal
523526
): Promise<WindchillRouteOutput> {
524527
const documentUrl = windchillDocumentUrl(body.baseUrl, body.documentOid)
@@ -562,7 +565,7 @@ async function executeDownload(
562565
export const POST = withRouteHandler(
563566
async (request: NextRequest) => {
564567
const requestId = generateRequestId()
565-
let principal: WorkflowExecutionDelegatedPrincipal
568+
let principal: BoundWorkflowExecutionDelegatedPrincipal
566569
try {
567570
principal = await authenticateWindchillExecutor(request)
568571
} catch (error) {
@@ -603,7 +606,11 @@ export const POST = withRouteHandler(
603606
body.operation === 'windchill_upload_primary_content'
604607
? [body.primaryFile]
605608
: body.attachmentFiles
606-
const files = await loadUploadFiles(inputs, principal.subjectUserId, requestId)
609+
const files = await loadUploadFiles(
610+
inputs,
611+
requirePrincipalSubjectUserId(principal),
612+
requestId
613+
)
607614
if (files instanceof NextResponse) return files
608615
const uploadedFileNames = await uploadWindchillContent({
609616
params: body,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,17 @@ export const POST = withRouteHandler(
283283
`Unexpected workflow authorization status: ${workflowAuthorization.status}`
284284
)
285285
}
286+
if (!workflowAuthorization.workflow.workspaceId) {
287+
throw new Error(`Workflow ${workflowId} has no workspace`)
288+
}
286289
result = await executeWorkflowService({
287290
workflowId,
291+
principal: {
292+
kind: 'system',
293+
serviceId: 'public_api',
294+
workspaceId: workflowAuthorization.workflow.workspaceId,
295+
workflowId,
296+
},
288297
userId,
289298
isPublicApiAccess,
290299
input: body.input ?? {},

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,32 @@ interface ExecutionCallerCase {
252252
isPublic?: boolean
253253
}
254254

255+
const SESSION_PRINCIPAL = {
256+
kind: 'session',
257+
userId: 'session-user-1',
258+
sessionId: 'session-1',
259+
} as const
260+
261+
const PERSONAL_API_KEY_PRINCIPAL = {
262+
kind: 'personal_api_key',
263+
userId: 'personal-key-user-1',
264+
keyId: 'personal-key-1',
265+
} as const
266+
267+
const WORKSPACE_API_KEY_PRINCIPAL = {
268+
kind: 'workspace_api_key',
269+
workspaceId: 'workspace-1',
270+
keyId: 'workspace-key-1',
271+
} as const
272+
255273
const EXECUTION_CALLERS: ExecutionCallerCase[] = [
256274
{
257275
caseName: 'session',
258276
authResult: {
259277
success: true,
260278
userId: 'session-user-1',
261279
authType: 'session',
280+
principal: SESSION_PRINCIPAL,
262281
},
263282
headers: { Cookie: 'session=value' },
264283
usesExternalInput: false,
@@ -270,6 +289,7 @@ const EXECUTION_CALLERS: ExecutionCallerCase[] = [
270289
userId: 'personal-key-user-1',
271290
authType: 'api_key',
272291
apiKeyType: 'personal',
292+
principal: PERSONAL_API_KEY_PRINCIPAL,
273293
},
274294
headers: { 'X-API-Key': 'personal-key' },
275295
usesExternalInput: true,
@@ -282,6 +302,7 @@ const EXECUTION_CALLERS: ExecutionCallerCase[] = [
282302
workspaceId: 'workspace-1',
283303
authType: 'api_key',
284304
apiKeyType: 'workspace',
305+
principal: WORKSPACE_API_KEY_PRINCIPAL,
285306
},
286307
headers: { 'X-API-Key': 'workspace-key' },
287308
usesExternalInput: true,
@@ -455,6 +476,7 @@ describe('workflow execute async route', () => {
455476
success: true,
456477
userId: 'session-user-1',
457478
authType: 'session',
479+
principal: SESSION_PRINCIPAL,
458480
})
459481

460482
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
@@ -1126,6 +1148,7 @@ describe('workflow execute async route', () => {
11261148
userId: 'personal-key-user-1',
11271149
authType: 'api_key',
11281150
apiKeyType: 'personal',
1151+
principal: PERSONAL_API_KEY_PRINCIPAL,
11291152
})
11301153
const response = await POST(
11311154
createMockRequest(
@@ -2494,6 +2517,11 @@ describe('workflow execute async route', () => {
24942517
userId: 'api-user-1',
24952518
authType: 'api_key',
24962519
apiKeyType: 'personal',
2520+
principal: {
2521+
kind: 'personal_api_key',
2522+
userId: 'api-user-1',
2523+
keyId: 'personal-key-1',
2524+
},
24972525
})
24982526
workflowsUtilsMockFns.mockWorkflowHasResponseBlock.mockReturnValueOnce(true)
24992527
workflowsUtilsMockFns.mockCreateHttpResponseFromBlock.mockResolvedValueOnce(

0 commit comments

Comments
 (0)