Skip to content

Commit 44eaa8a

Browse files
feat(workflows): preserve execution principals (#6891)
* feat(workflows): preserve execution principals * fix(credential-groups): list all managed credentials * fix(credential-groups): restore email credential filtering * fix(workflows): resume legacy execution snapshots * test(workflows): align execution principal fixtures * fix(workflows): keep snapshot metadata first * fix(credential-groups): resolve external actor enrollments * fix(workflows): resume legacy queued jobs * fix(workflows): preserve execution principal authority * chore(core): format principal operation types * fix(workflows): restore legacy paused api key actors * chore(design): remove principal design documents
1 parent 72363b3 commit 44eaa8a

71 files changed

Lines changed: 2306 additions & 167 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,21 @@ function createPausedExecution(overrides: PausedExecutionOverrides = {}) {
113113
executionId: overrides.executionId ?? EXECUTION_ID,
114114
executionSnapshot: {
115115
snapshot: JSON.stringify({
116+
version: 1,
116117
metadata: {
117118
requestId: 'request-original',
118119
workflowId: overrides.snapshotWorkflowId ?? WORKFLOW_ID,
119120
executionId: overrides.snapshotExecutionId ?? EXECUTION_ID,
120121
workspaceId: overrides.snapshotWorkspaceId ?? WORKSPACE_ID,
121122
userId: overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID,
123+
principal: {
124+
version: 1,
125+
principal: {
126+
kind: 'session',
127+
userId: overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID,
128+
sessionId: 'session-original',
129+
},
130+
},
122131
billingAttribution,
123132
triggerType: 'manual',
124133
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/[workflowId]/execute/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,17 @@ export const POST = withRouteHandler(
363363
`Unexpected workflow authorization status: ${workflowAuthorization.status}`
364364
)
365365
}
366+
if (!workflowAuthorization.workflow.workspaceId) {
367+
throw new Error(`Workflow ${workflowId} has no workspace`)
368+
}
366369
result = await executeWorkflowService({
367370
workflowId,
371+
principal: {
372+
kind: 'system',
373+
serviceId: 'public_api',
374+
workspaceId: workflowAuthorization.workflow.workspaceId,
375+
workflowId,
376+
},
368377
userId: publicApiUserId,
369378
isPublicApiAccess,
370379
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
@@ -255,13 +255,32 @@ interface ExecutionCallerCase {
255255
isPublic?: boolean
256256
}
257257

258+
const SESSION_PRINCIPAL = {
259+
kind: 'session',
260+
userId: 'session-user-1',
261+
sessionId: 'session-1',
262+
} as const
263+
264+
const PERSONAL_API_KEY_PRINCIPAL = {
265+
kind: 'personal_api_key',
266+
userId: 'personal-key-user-1',
267+
keyId: 'personal-key-1',
268+
} as const
269+
270+
const WORKSPACE_API_KEY_PRINCIPAL = {
271+
kind: 'workspace_api_key',
272+
workspaceId: 'workspace-1',
273+
keyId: 'workspace-key-1',
274+
} as const
275+
258276
const EXECUTION_CALLERS: ExecutionCallerCase[] = [
259277
{
260278
caseName: 'session',
261279
authResult: {
262280
success: true,
263281
userId: 'session-user-1',
264282
authType: 'session',
283+
principal: SESSION_PRINCIPAL,
265284
},
266285
headers: { Cookie: 'session=value' },
267286
usesExternalInput: false,
@@ -273,6 +292,7 @@ const EXECUTION_CALLERS: ExecutionCallerCase[] = [
273292
userId: 'personal-key-user-1',
274293
authType: 'api_key',
275294
apiKeyType: 'personal',
295+
principal: PERSONAL_API_KEY_PRINCIPAL,
276296
},
277297
headers: { 'X-API-Key': 'personal-key' },
278298
usesExternalInput: true,
@@ -285,6 +305,7 @@ const EXECUTION_CALLERS: ExecutionCallerCase[] = [
285305
workspaceId: 'workspace-1',
286306
authType: 'api_key',
287307
apiKeyType: 'workspace',
308+
principal: WORKSPACE_API_KEY_PRINCIPAL,
288309
},
289310
headers: { 'X-API-Key': 'workspace-key' },
290311
usesExternalInput: true,
@@ -458,6 +479,7 @@ describe('workflow execute async route', () => {
458479
success: true,
459480
userId: 'session-user-1',
460481
authType: 'session',
482+
principal: SESSION_PRINCIPAL,
461483
})
462484

463485
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
@@ -1309,6 +1331,7 @@ describe('workflow execute async route', () => {
13091331
userId: 'personal-key-user-1',
13101332
authType: 'api_key',
13111333
apiKeyType: 'personal',
1334+
principal: PERSONAL_API_KEY_PRINCIPAL,
13121335
})
13131336
const response = await POST(
13141337
createMockRequest(
@@ -2677,6 +2700,11 @@ describe('workflow execute async route', () => {
26772700
userId: 'api-user-1',
26782701
authType: 'api_key',
26792702
apiKeyType: 'personal',
2703+
principal: {
2704+
kind: 'personal_api_key',
2705+
userId: 'api-user-1',
2706+
keyId: 'personal-key-1',
2707+
},
26802708
})
26812709
workflowsUtilsMockFns.mockWorkflowHasResponseBlock.mockReturnValueOnce(true)
26822710
workflowsUtilsMockFns.mockCreateHttpResponseFromBlock.mockResolvedValueOnce(

0 commit comments

Comments
 (0)