Skip to content
28 changes: 17 additions & 11 deletions apps/sim/app/api/auth/oauth/token/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
import {
resolvePrincipalSubject,
type WorkflowExecutionDelegatedPrincipal,
} from '@sim/auth/principal'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
Expand Down Expand Up @@ -206,16 +209,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
request,
})

captureServerEvent(
managedOAuthPrincipal.subjectUserId,
'credential_used',
{
credential_type: 'managed_oauth',
provider_id: toolMetadata.oauth.provider,
workspace_id: managedOAuthPrincipal.workspaceId,
},
{ groups: { workspace: managedOAuthPrincipal.workspaceId } }
)
const managedOAuthSubject = resolvePrincipalSubject(managedOAuthPrincipal)
if (managedOAuthSubject?.kind === 'sim_user') {
captureServerEvent(
managedOAuthSubject.userId,
'credential_used',
{
credential_type: 'managed_oauth',
provider_id: toolMetadata.oauth.provider,
workspace_id: managedOAuthPrincipal.workspaceId,
},
{ groups: { workspace: managedOAuthPrincipal.workspaceId } }
)
}

return NextResponse.json(
{
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/app/api/chat/[identifier]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ export const POST = withRouteHandler(
resolvedActorUserId,
{
enabled: true,
principal: {
kind: 'system',
serviceId: 'chat',
workspaceId,
workflowId: deployment.workflowId,
},
selectedOutputs,
isSecureMode: true,
workflowTriggerType: 'chat',
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/files/uploads/purposes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ async function principalUserId(principal: Principal, workspaceId?: string): Prom
}
case 'delegated':
throw new UploadSessionError('forbidden', 'Delegated principals cannot create uploads')
case 'system':
throw new UploadSessionError('forbidden', 'System principals cannot create uploads')
case 'credential_group_enrollment':
throw new UploadSessionError(
'forbidden',
Expand Down
25 changes: 25 additions & 0 deletions apps/sim/app/api/mcp/serve/[serverId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ function createResolvedSecretTraceProvenance(userId: string, workspaceId = 'ws-1
}
}

const SESSION_PRINCIPAL = {
kind: 'session',
userId: 'user-1',
sessionId: 'session-1',
} as const

const PERSONAL_API_KEY_PRINCIPAL = {
kind: 'personal_api_key',
userId: 'user-1',
keyId: 'personal-key-1',
} as const

const WORKSPACE_API_KEY_PRINCIPAL = {
kind: 'workspace_api_key',
workspaceId: 'ws-1',
keyId: 'workspace-key-1',
} as const

vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)

vi.mock('@/lib/auth/internal', () => ({
Expand Down Expand Up @@ -215,6 +233,7 @@ describe('MCP Serve Route', () => {
success: true,
userId: 'user-1',
authType: 'session',
principal: SESSION_PRINCIPAL,
})
mockGetUserEntityPermissions.mockResolvedValueOnce('read')

Expand Down Expand Up @@ -273,6 +292,7 @@ describe('MCP Serve Route', () => {
userId: 'user-1',
authType: 'api_key',
apiKeyType: 'personal',
principal: PERSONAL_API_KEY_PRINCIPAL,
})
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
mockExecuteWorkflowService.mockResolvedValueOnce({
Expand Down Expand Up @@ -334,6 +354,7 @@ describe('MCP Serve Route', () => {
userId: 'user-1',
authType: 'api_key',
apiKeyType: 'personal',
principal: PERSONAL_API_KEY_PRINCIPAL,
})
mockGetUserEntityPermissions.mockResolvedValueOnce('write')

Expand Down Expand Up @@ -375,6 +396,7 @@ describe('MCP Serve Route', () => {
authType: 'api_key',
apiKeyType: 'workspace',
workspaceId: 'ws-1',
principal: WORKSPACE_API_KEY_PRINCIPAL,
})
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
mockExecuteWorkflowService.mockResolvedValueOnce({
Expand Down Expand Up @@ -477,6 +499,7 @@ describe('MCP Serve Route', () => {
success: true,
userId: 'user-1',
authType: 'session',
principal: SESSION_PRINCIPAL,
})
mockGetUserEntityPermissions.mockResolvedValueOnce('read')
mockExecuteWorkflowService.mockResolvedValueOnce({
Expand Down Expand Up @@ -1096,6 +1119,7 @@ describe('MCP Serve Route', () => {
userId: 'user-1',
authType: 'api_key',
apiKeyType: 'personal',
principal: PERSONAL_API_KEY_PRINCIPAL,
})
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
mockExecuteWorkflowService.mockResolvedValueOnce({
Expand Down Expand Up @@ -1193,6 +1217,7 @@ describe('MCP Serve Route', () => {
authType: 'api_key',
apiKeyType: 'workspace',
workspaceId: 'ws-1',
principal: WORKSPACE_API_KEY_PRINCIPAL,
})
mockGetUserEntityPermissions.mockResolvedValueOnce('write')
mockExecuteWorkflowService.mockResolvedValueOnce({
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/app/api/mcp/serve/[serverId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
SUPPORTED_PROTOCOL_VERSIONS,
type Tool,
} from '@modelcontextprotocol/sdk/types.js'
import type { WorkflowExecutionPrincipal } from '@sim/auth/principal'
import { db } from '@sim/db'
import {
workflow,
Expand Down Expand Up @@ -96,6 +97,7 @@ interface RouteParams {
interface ExecuteAuthContext {
userId: string
useAuthenticatedUserAsActor: boolean
principal: WorkflowExecutionPrincipal
}

function createResponse(id: RequestId, result: unknown): JSONRPCResultResponse {
Expand Down Expand Up @@ -364,6 +366,9 @@ async function authorizeMcpServeRequest(
if (!auth.success || !auth.userId) {
return { response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }
}
if (!auth.principal) {
throw new Error('Authenticated MCP request is missing its principal')
}
Comment thread
cursor[bot] marked this conversation as resolved.

if (server.isPublic) return {}

Expand Down Expand Up @@ -396,6 +401,7 @@ async function authorizeMcpServeRequest(
executeAuthContext: {
userId: auth.userId,
useAuthenticatedUserAsActor: isPersonalApiKey,
principal: auth.principal,
},
}
}
Expand Down Expand Up @@ -856,6 +862,14 @@ async function handleToolsCall(
*/
const serviceResult = await executeWorkflowService({
workflowId: tool.workflowId,
principal:
executeAuthContext?.principal ??
({
kind: 'system',
serviceId: 'public_api',
workspaceId: wf.workspaceId,
workflowId: tool.workflowId,
} satisfies WorkflowExecutionPrincipal),
userId: actorUserId,
input: workflowInput,
triggerType: 'mcp',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,21 @@ function createPausedExecution(overrides: PausedExecutionOverrides = {}) {
executionId: overrides.executionId ?? EXECUTION_ID,
executionSnapshot: {
snapshot: JSON.stringify({
version: 1,
metadata: {
requestId: 'request-original',
workflowId: overrides.snapshotWorkflowId ?? WORKFLOW_ID,
executionId: overrides.snapshotExecutionId ?? EXECUTION_ID,
workspaceId: overrides.snapshotWorkspaceId ?? WORKSPACE_ID,
userId: overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID,
principal: {
version: 1,
principal: {
kind: 'session',
userId: overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID,
sessionId: 'session-original',
},
},
billingAttribution,
triggerType: 'manual',
useDraftState: false,
Expand Down
27 changes: 17 additions & 10 deletions apps/sim/app/api/tools/windchill/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
import {
type BoundWorkflowExecutionDelegatedPrincipal,
requirePrincipalSubjectUserId,
} from '@sim/auth/principal'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
Expand Down Expand Up @@ -53,16 +56,16 @@ const windchillSessionOrExecutorAuth = createInternalSessionOrExecutorAuth({

async function authenticateWindchillExecutor(
request: NextRequest
): Promise<WorkflowExecutionDelegatedPrincipal> {
): Promise<BoundWorkflowExecutionDelegatedPrincipal> {
const principal = await windchillSessionOrExecutorAuth.authenticate(request, {})
if (
principal.kind !== 'delegated' ||
principal.serviceId !== 'executor' ||
!('delegationContext' in principal)
!principal.delegationContext
) {
throw new InternalUnauthenticatedError('Authentication required')
}
return principal
return { ...principal, delegationContext: principal.delegationContext }
}

type WindchillRouteOutput = Extract<WindchillOperationResponse, { success: true }>['output']
Expand Down Expand Up @@ -485,7 +488,7 @@ async function storeDownloadedFile({
fileName,
contentType,
}: {
principal: WorkflowExecutionDelegatedPrincipal
principal: BoundWorkflowExecutionDelegatedPrincipal
buffer: Buffer
fileName: string
contentType: string
Expand All @@ -501,14 +504,14 @@ async function storeDownloadedFile({
buffer,
fileName,
contentType,
principal.subjectUserId
requirePrincipalSubjectUserId(principal)
)
}
return uploadCopilotFile({
buffer,
fileName,
contentType,
userId: principal.subjectUserId,
userId: requirePrincipalSubjectUserId(principal),
})
}

Expand All @@ -518,7 +521,7 @@ async function executeDownload(
| { operation: 'windchill_download_primary_content' }
| { operation: 'windchill_download_attachment' }
>,
principal: WorkflowExecutionDelegatedPrincipal,
principal: BoundWorkflowExecutionDelegatedPrincipal,
signal: AbortSignal
): Promise<WindchillRouteOutput> {
const documentUrl = windchillDocumentUrl(body.baseUrl, body.documentOid)
Expand Down Expand Up @@ -562,7 +565,7 @@ async function executeDownload(
export const POST = withRouteHandler(
async (request: NextRequest) => {
const requestId = generateRequestId()
let principal: WorkflowExecutionDelegatedPrincipal
let principal: BoundWorkflowExecutionDelegatedPrincipal
try {
principal = await authenticateWindchillExecutor(request)
} catch (error) {
Expand Down Expand Up @@ -603,7 +606,11 @@ export const POST = withRouteHandler(
body.operation === 'windchill_upload_primary_content'
? [body.primaryFile]
: body.attachmentFiles
const files = await loadUploadFiles(inputs, principal.subjectUserId, requestId)
const files = await loadUploadFiles(
inputs,
requirePrincipalSubjectUserId(principal),
requestId
)
if (files instanceof NextResponse) return files
const uploadedFileNames = await uploadWindchillContent({
params: body,
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/app/api/v2/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,17 @@ export const POST = withRouteHandler(
`Unexpected workflow authorization status: ${workflowAuthorization.status}`
)
}
if (!workflowAuthorization.workflow.workspaceId) {
throw new Error(`Workflow ${workflowId} has no workspace`)
}
result = await executeWorkflowService({
workflowId,
principal: {
kind: 'system',
serviceId: 'public_api',
workspaceId: workflowAuthorization.workflow.workspaceId,
workflowId,
},
userId,
isPublicApiAccess,
input: body.input ?? {},
Expand Down
28 changes: 28 additions & 0 deletions apps/sim/app/api/workflows/[id]/execute/route.async.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,32 @@ interface ExecutionCallerCase {
isPublic?: boolean
}

const SESSION_PRINCIPAL = {
kind: 'session',
userId: 'session-user-1',
sessionId: 'session-1',
} as const

const PERSONAL_API_KEY_PRINCIPAL = {
kind: 'personal_api_key',
userId: 'personal-key-user-1',
keyId: 'personal-key-1',
} as const

const WORKSPACE_API_KEY_PRINCIPAL = {
kind: 'workspace_api_key',
workspaceId: 'workspace-1',
keyId: 'workspace-key-1',
} as const

const EXECUTION_CALLERS: ExecutionCallerCase[] = [
{
caseName: 'session',
authResult: {
success: true,
userId: 'session-user-1',
authType: 'session',
principal: SESSION_PRINCIPAL,
},
headers: { Cookie: 'session=value' },
usesExternalInput: false,
Expand All @@ -270,6 +289,7 @@ const EXECUTION_CALLERS: ExecutionCallerCase[] = [
userId: 'personal-key-user-1',
authType: 'api_key',
apiKeyType: 'personal',
principal: PERSONAL_API_KEY_PRINCIPAL,
},
headers: { 'X-API-Key': 'personal-key' },
usesExternalInput: true,
Expand All @@ -282,6 +302,7 @@ const EXECUTION_CALLERS: ExecutionCallerCase[] = [
workspaceId: 'workspace-1',
authType: 'api_key',
apiKeyType: 'workspace',
principal: WORKSPACE_API_KEY_PRINCIPAL,
},
headers: { 'X-API-Key': 'workspace-key' },
usesExternalInput: true,
Expand Down Expand Up @@ -455,6 +476,7 @@ describe('workflow execute async route', () => {
success: true,
userId: 'session-user-1',
authType: 'session',
principal: SESSION_PRINCIPAL,
})

mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
Expand Down Expand Up @@ -1144,6 +1166,7 @@ describe('workflow execute async route', () => {
userId: 'personal-key-user-1',
authType: 'api_key',
apiKeyType: 'personal',
principal: PERSONAL_API_KEY_PRINCIPAL,
})
const response = await POST(
createMockRequest(
Expand Down Expand Up @@ -2512,6 +2535,11 @@ describe('workflow execute async route', () => {
userId: 'api-user-1',
authType: 'api_key',
apiKeyType: 'personal',
principal: {
kind: 'personal_api_key',
userId: 'api-user-1',
keyId: 'personal-key-1',
},
})
workflowsUtilsMockFns.mockWorkflowHasResponseBlock.mockReturnValueOnce(true)
workflowsUtilsMockFns.mockCreateHttpResponseFromBlock.mockResolvedValueOnce(
Expand Down
Loading
Loading