Skip to content

Commit 155b89a

Browse files
fix(auth): clarify workflow execution authorization
1 parent 9304304 commit 155b89a

14 files changed

Lines changed: 129 additions & 129 deletions

File tree

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,27 +106,39 @@ function createPausedExecution(overrides: PausedExecutionOverrides = {}) {
106106
'billingAttribution' in overrides
107107
? overrides.billingAttribution
108108
: structuredClone(PERSISTED_ATTRIBUTION)
109+
const snapshotWorkflowId = overrides.snapshotWorkflowId ?? WORKFLOW_ID
110+
const snapshotExecutionId = overrides.snapshotExecutionId ?? EXECUTION_ID
111+
const snapshotActorUserId = overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID
109112

110113
return {
111114
id: 'paused-execution-1',
112115
workflowId: overrides.workflowId ?? WORKFLOW_ID,
113116
executionId: overrides.executionId ?? EXECUTION_ID,
114117
executionSnapshot: {
115118
snapshot: JSON.stringify({
116-
version: 1,
119+
version: 2,
117120
metadata: {
118121
requestId: 'request-original',
119-
workflowId: overrides.snapshotWorkflowId ?? WORKFLOW_ID,
120-
executionId: overrides.snapshotExecutionId ?? EXECUTION_ID,
122+
workflowId: snapshotWorkflowId,
123+
executionId: snapshotExecutionId,
121124
workspaceId: overrides.snapshotWorkspaceId ?? WORKSPACE_ID,
122-
userId: overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID,
125+
userId: snapshotActorUserId,
123126
principal: {
124-
version: 1,
127+
version: 2,
125128
principal: {
126129
kind: 'session',
127-
userId: overrides.snapshotActorUserId ?? PERSISTED_ACTOR_ID,
130+
userId: snapshotActorUserId,
128131
sessionId: 'session-original',
129132
},
133+
executionMetadata: {
134+
executionId: snapshotExecutionId,
135+
rootWorkflowId: snapshotWorkflowId,
136+
currentWorkflow: {
137+
workflowId: snapshotWorkflowId,
138+
mode: 'deployment',
139+
deploymentVersionId: 'deployment-version-1',
140+
},
141+
},
130142
},
131143
billingAttribution,
132144
triggerType: 'manual',

‎apps/sim/app/api/table/[tableId]/route.test.ts‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { hybridAuthMockFns } from '@sim/testing'
55
import { NextRequest } from 'next/server'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { createTestRuntimePrincipal } from '@/lib/auth/runtime-principal.test-support'
78

89
const {
910
mockCheckAccess,
@@ -15,6 +16,7 @@ const {
1516
mockFindActiveFolder,
1617
mockGetLimits,
1718
mockAuthenticate,
19+
mockAuthenticateWithTransport,
1820
mockReadTable,
1921
} = vi.hoisted(() => ({
2022
mockCheckAccess: vi.fn(),
@@ -26,11 +28,15 @@ const {
2628
mockFindActiveFolder: vi.fn(),
2729
mockGetLimits: vi.fn(),
2830
mockAuthenticate: vi.fn(),
31+
mockAuthenticateWithTransport: vi.fn(),
2932
mockReadTable: vi.fn(),
3033
}))
3134

3235
vi.mock('@/lib/table/api', () => ({
33-
internalTableSessionOrExecutorAuth: { authenticate: mockAuthenticate },
36+
internalTableSessionOrExecutorAuth: {
37+
authenticate: mockAuthenticate,
38+
authenticateWithTransport: mockAuthenticateWithTransport,
39+
},
3440
internalTableErrorPolicies: {
3541
concealTableAuthorization: { project: () => null },
3642
},
@@ -182,15 +188,16 @@ describe('PATCH /api/table/[tableId] folder moves', () => {
182188
describe('GET /api/table/[tableId] application adapter', () => {
183189
beforeEach(() => {
184190
vi.clearAllMocks()
185-
mockAuthenticate.mockResolvedValue({
186-
kind: 'delegated',
187-
serviceId: 'executor',
188-
subjectUserId: 'user-1',
189-
workspaceId: 'workspace-canonical',
190-
delegationId: 'delegation-1',
191-
audience: 'sim:tables',
192-
issuedAt: new Date('2026-01-01'),
193-
expiresAt: new Date('2026-01-02'),
191+
const principal = createTestRuntimePrincipal({
192+
principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
193+
executionId: 'execution-1',
194+
rootWorkflowId: 'workflow-1',
195+
})
196+
mockAuthenticate.mockResolvedValue(principal)
197+
mockAuthenticateWithTransport.mockResolvedValue({
198+
principal,
199+
transport: 'executor_jwt',
200+
executionWorkspaceId: 'workspace-canonical',
194201
})
195202
mockReadTable.mockResolvedValue({
196203
table: {

‎apps/sim/app/api/table/route.test.ts‎

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@
44

55
import { NextRequest } from 'next/server'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { createTestRuntimePrincipal } from '@/lib/auth/runtime-principal.test-support'
78

89
const mocks = vi.hoisted(() => ({
910
authenticate: vi.fn(),
11+
authenticateWithTransport: vi.fn(),
1012
createTable: vi.fn(),
1113
listTables: vi.fn(),
1214
capture: vi.fn(),
1315
}))
1416

1517
vi.mock('@/lib/table/api', () => ({
16-
internalTableSessionOrExecutorAuth: { authenticate: mocks.authenticate },
18+
internalTableSessionOrExecutorAuth: {
19+
authenticate: mocks.authenticate,
20+
authenticateWithTransport: mocks.authenticateWithTransport,
21+
},
1722
}))
1823

1924
vi.mock('@/lib/table/application/tables', () => ({
@@ -47,46 +52,51 @@ const TABLE = {
4752
}
4853

4954
function sessionPrincipal() {
50-
mocks.authenticate.mockResolvedValue({
55+
const principal = {
5156
kind: 'session',
5257
userId: 'user-1',
5358
sessionId: 'session-1',
54-
})
59+
} as const
60+
mocks.authenticate.mockResolvedValue(principal)
61+
mocks.authenticateWithTransport.mockResolvedValue({ principal, transport: 'session' })
5562
}
5663

5764
function executorPrincipal() {
58-
mocks.authenticate.mockResolvedValue({
59-
kind: 'delegated',
60-
serviceId: 'executor',
61-
subjectUserId: 'user-1',
62-
workspaceId: 'workspace-canonical',
63-
delegationId: 'delegation-1',
64-
audience: 'sim:tables',
65-
issuedAt: new Date('2026-01-01'),
66-
expiresAt: new Date('2026-01-02'),
65+
const principal = createTestRuntimePrincipal({
66+
principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
67+
executionId: 'execution-1',
68+
rootWorkflowId: 'parent-workflow',
69+
})
70+
mocks.authenticate.mockResolvedValue(principal)
71+
mocks.authenticateWithTransport.mockResolvedValue({
72+
principal,
73+
transport: 'executor_jwt',
74+
executionWorkspaceId: 'workspace-canonical',
6775
})
6876
}
6977

7078
function actorlessExecutorPrincipal() {
71-
mocks.authenticate.mockResolvedValue({
72-
kind: 'delegated',
73-
serviceId: 'executor',
74-
workspaceId: 'workspace-canonical',
75-
delegationId: 'delegation-1',
76-
audience: 'sim:tables',
77-
issuedAt: new Date('2026-01-01'),
78-
expiresAt: new Date('2026-01-02'),
79-
delegationContext: {
80-
kind: 'workflow_execution',
79+
const principal = createTestRuntimePrincipal({
80+
principal: {
81+
kind: 'system',
82+
serviceId: 'schedule',
83+
workspaceId: 'workspace-canonical',
8184
workflowId: 'parent-workflow',
82-
principal: {
83-
kind: 'system',
84-
serviceId: 'internal',
85-
workspaceId: 'workspace-canonical',
86-
workflowId: 'parent-workflow',
87-
},
85+
},
86+
executionId: 'execution-1',
87+
rootWorkflowId: 'parent-workflow',
88+
currentWorkflow: {
89+
workflowId: 'parent-workflow',
90+
mode: 'deployment',
91+
deploymentVersionId: 'deployment-version-1',
8892
},
8993
})
94+
mocks.authenticate.mockResolvedValue(principal)
95+
mocks.authenticateWithTransport.mockResolvedValue({
96+
principal,
97+
transport: 'executor_jwt',
98+
executionWorkspaceId: 'workspace-canonical',
99+
})
90100
}
91101

92102
function post(body: unknown) {
@@ -149,8 +159,8 @@ describe('/api/table application adapter', () => {
149159
expect(response.status).toBe(200)
150160
expect(mocks.createTable.mock.calls[0][0]).toMatchObject({
151161
principal: {
152-
kind: 'delegated',
153-
serviceId: 'executor',
162+
kind: 'system',
163+
serviceId: 'schedule',
154164
workspaceId: 'workspace-canonical',
155165
},
156166
input: { workspaceId: 'workspace-canonical' },

‎apps/sim/app/api/table/table-tool-auth.test.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* things have to line up for that to work:
66
*
77
* 1. the tool must declare an in-process operation, and
8-
* 2. the operation's policy must admit the `executor` delegated service.
8+
* 2. the operation's policy must admit a runtime workflow principal.
99
*/
1010
import { describe, expect, it } from 'vitest'
1111
import { tableOperations } from '@/lib/table/application/operations'
@@ -29,10 +29,9 @@ describe('executor access to the migrated table row routes', () => {
2929
})
3030

3131
it.each(EXECUTOR_ROW_TOOLS)(
32-
'%s runs under an operation that admits the executor',
32+
'%s runs under an operation that admits workflow execution',
3333
(_name, _tool, operation) => {
34-
expect(operation.delegatedServices).toContain('executor')
35-
expect(operation.principalKinds).toContain('delegated')
34+
expect(operation.workflowExecution).toBe('allow')
3635
}
3736
)
3837
})

‎apps/sim/app/api/workflows/[id]/deployed/route.test.ts‎

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
import { authMockFns } from '@sim/testing'
55
import { NextRequest } from 'next/server'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { createTestRuntimePrincipal } from '@/lib/auth/runtime-principal.test-support'
78
import { OrchestrationError } from '@/lib/core/orchestration/types'
89

910
const {
1011
InvalidDelegationTokenError,
11-
mockBindExecutorDelegation,
12+
mockBindExecutorDelegationAdmission,
1213
mockReadWorkflowDefinition,
1314
mockVerifyDelegationToken,
1415
} = vi.hoisted(() => ({
1516
InvalidDelegationTokenError: class InvalidDelegationTokenError extends Error {},
16-
mockBindExecutorDelegation: vi.fn(),
17+
mockBindExecutorDelegationAdmission: vi.fn(),
1718
mockReadWorkflowDefinition: vi.fn(),
1819
mockVerifyDelegationToken: vi.fn(),
1920
}))
@@ -24,7 +25,7 @@ vi.mock('@/lib/auth/internal', () => ({
2425
}))
2526

2627
vi.mock('@/lib/auth/internal-delegation', () => ({
27-
bindInternalExecutorDelegation: mockBindExecutorDelegation,
28+
bindInternalExecutorDelegationAdmission: mockBindExecutorDelegationAdmission,
2829
InvalidInternalDelegationBindingError: class InvalidInternalDelegationBindingError extends Error {},
2930
}))
3031

@@ -34,7 +35,8 @@ vi.mock('@/lib/workflows/application/read-workflow-definition', () => {
3435
minimumRole: 'read',
3536
workspaceApiKey: 'allow',
3637
principalKinds: ['session', 'personal_api_key', 'workspace_api_key', 'delegated'],
37-
delegatedServices: ['copilot', 'executor'],
38+
delegatedServices: ['copilot'],
39+
workflowExecution: 'allow',
3840
} as const
3941
return {
4042
readWorkflowDefinition: { operation, execute: mockReadWorkflowDefinition },
@@ -57,21 +59,11 @@ const SESSION = {
5759
session: { id: 'session-123' },
5860
}
5961

60-
const EXECUTOR_PRINCIPAL = {
61-
kind: 'delegated' as const,
62-
serviceId: 'executor' as const,
63-
subjectUserId: 'user-123',
64-
workspaceId: 'workspace-456',
65-
delegationId: 'delegation-123',
66-
audience: 'sim:workflows',
67-
issuedAt: new Date('2026-08-08T00:00:00.000Z'),
68-
expiresAt: new Date('2999-08-08T00:00:00.000Z'),
69-
delegationContext: {
70-
kind: 'workflow_execution' as const,
71-
workflowId: 'origin-workflow',
72-
executionId: 'origin-run',
73-
},
74-
}
62+
const EXECUTOR_PRINCIPAL = createTestRuntimePrincipal({
63+
principal: { kind: 'session', userId: 'user-123', sessionId: 'session-123' },
64+
rootWorkflowId: 'origin-workflow',
65+
executionId: 'origin-run',
66+
})
7567

7668
function createRequest(bearerToken?: string) {
7769
return new NextRequest('http://localhost:3000/api/workflows/workflow-123/deployed', {
@@ -94,12 +86,18 @@ describe('GET /api/workflows/[id]/deployed', () => {
9486
vi.clearAllMocks()
9587
authMockFns.mockGetSession.mockResolvedValue(SESSION)
9688
mockReadWorkflowDefinition.mockResolvedValue(readResult())
97-
mockVerifyDelegationToken.mockResolvedValue({
98-
subjectUserId: 'user-123',
99-
workflowId: 'origin-workflow',
100-
executionId: 'origin-run',
89+
const delegation = {
90+
serviceId: 'executor' as const,
91+
principal: EXECUTOR_PRINCIPAL,
92+
delegationId: 'delegation-123',
93+
issuedAt: new Date('2026-08-08T00:00:00.000Z'),
94+
expiresAt: new Date('2999-08-08T00:00:00.000Z'),
95+
}
96+
mockVerifyDelegationToken.mockResolvedValue(delegation)
97+
mockBindExecutorDelegationAdmission.mockResolvedValue({
98+
principal: EXECUTOR_PRINCIPAL,
99+
workspaceId: 'workspace-456',
101100
})
102-
mockBindExecutorDelegation.mockResolvedValue(EXECUTOR_PRINCIPAL)
103101
})
104102

105103
it('passes the authenticated session principal through the application use case', async () => {
@@ -119,9 +117,8 @@ describe('GET /api/workflows/[id]/deployed', () => {
119117
const response = await GET(createRequest('signed-token'), routeParams())
120118

121119
expect(response.status).toBe(200)
122-
expect(mockBindExecutorDelegation).toHaveBeenCalledWith(
123-
expect.objectContaining({ workflowId: 'origin-workflow', executionId: 'origin-run' }),
124-
{ audience: 'sim:workflows', resourceScope: undefined }
120+
expect(mockBindExecutorDelegationAdmission).toHaveBeenCalledWith(
121+
expect.objectContaining({ principal: EXECUTOR_PRINCIPAL })
125122
)
126123
expect(mockReadWorkflowDefinition).toHaveBeenCalledWith(
127124
expect.objectContaining({

‎apps/sim/executor/handlers/credential-group/credential-group-handler.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('CredentialGroupBlockHandler', () => {
8787
)
8888
})
8989

90-
it('lists credentials with an optional email selector', async () => {
90+
it('keeps the selected group on application input instead of principal metadata', async () => {
9191
mocks.listCredentials.mockResolvedValue({
9292
credentials: [],
9393
count: 0,

‎apps/sim/lib/credential-groups/application/authorization.ts‎

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import {
66
type WorkflowExecutionAuthority,
77
type WorkflowExecutionPrincipal,
88
} from '@sim/auth/principal'
9-
import type {
10-
WorkspaceAuthorizationContext,
11-
WorkspaceDelegationPolicy,
12-
} from '@/lib/core/application'
9+
import type { WorkspaceAuthorizationContext } from '@/lib/core/application'
1310
import { OrchestrationError } from '@/lib/core/orchestration/types'
1411
import {
1512
credentialGroupWorkflowAccessPolicyCodec,
@@ -28,8 +25,6 @@ import {
2825
import type { ResourcePolicyBindingFor } from '@/lib/resource-policies/registry'
2926
import { requireResourcePolicy } from '@/lib/resource-policies/repository'
3027

31-
export const CREDENTIAL_GROUP_DELEGATION_AUDIENCE = 'sim:credential-groups'
32-
3328
export interface CredentialGroupAuthorizationContext extends WorkspaceAuthorizationContext {
3429
credentialGroupId: string
3530
}
@@ -160,17 +155,3 @@ export async function requireCredentialGroupCredentialAccess(
160155
throw new OrchestrationError('forbidden', 'Credential Group credential access denied')
161156
}
162157
}
163-
164-
export const credentialGroupDelegationPolicy = {
165-
audience: CREDENTIAL_GROUP_DELEGATION_AUDIENCE,
166-
isWithinScope: (
167-
principal: Extract<Principal, { kind: 'delegated' }>,
168-
context: CredentialGroupApplicationContext
169-
) => principal.resourceScope?.credentialGroupId === context.credentialGroupId,
170-
} satisfies WorkspaceDelegationPolicy<CredentialGroupApplicationContext>
171-
172-
export const credentialGroupWorkspaceDelegationPolicy = {
173-
audience: CREDENTIAL_GROUP_DELEGATION_AUDIENCE,
174-
isWithinScope: (principal: Extract<Principal, { kind: 'delegated' }>) =>
175-
principal.resourceScope?.credentialGroupId === undefined,
176-
} satisfies WorkspaceDelegationPolicy<WorkspaceAuthorizationContext>

0 commit comments

Comments
 (0)