Skip to content

Commit 79a871a

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(windchill): inline route authentication
1 parent 2c504d2 commit 79a871a

4 files changed

Lines changed: 104 additions & 159 deletions

File tree

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

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
/**
22
* @vitest-environment node
33
*/
4-
import { createMockRequest } from '@sim/testing'
4+
import { createMockRequest as createTestingRequest, resetEnvMock } from '@sim/testing'
55
import { NextResponse } from 'next/server'
6-
import { beforeEach, describe, expect, it, vi } from 'vitest'
7-
import { InternalUnauthenticatedError } from '@/lib/api/server/routes'
6+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
87
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
98
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
109

1110
const {
11+
MockInvalidBindingError,
1212
MockWindchillProviderError,
13-
mockAuthenticateWindchill,
1413
mockAssertToolFileAccess,
14+
mockBindDelegation,
1515
mockCreateWindchillSession,
1616
mockDownloadServableFileFromStorage,
1717
mockDownloadWindchillContent,
18+
mockGetSession,
1819
mockProcessFilesToUserFiles,
1920
mockUploadCopilotFile,
2021
mockUploadExecutionFile,
2122
mockUploadWindchillContent,
2223
mockWindchillMutationRequest,
2324
} = vi.hoisted(() => {
25+
class MockInvalidBindingError extends Error {}
2426
class MockWindchillProviderError extends Error {
2527
constructor(
2628
message: string,
@@ -32,12 +34,14 @@ const {
3234
}
3335

3436
return {
37+
MockInvalidBindingError,
3538
MockWindchillProviderError,
36-
mockAuthenticateWindchill: vi.fn(),
3739
mockAssertToolFileAccess: vi.fn(),
40+
mockBindDelegation: vi.fn(),
3841
mockCreateWindchillSession: vi.fn(),
3942
mockDownloadServableFileFromStorage: vi.fn(),
4043
mockDownloadWindchillContent: vi.fn(),
44+
mockGetSession: vi.fn(),
4145
mockProcessFilesToUserFiles: vi.fn(),
4246
mockUploadCopilotFile: vi.fn(),
4347
mockUploadExecutionFile: vi.fn(),
@@ -46,9 +50,12 @@ const {
4650
}
4751
})
4852

49-
vi.mock('@/lib/windchill/api/route-policies', () => ({
50-
internalWindchillExecutorAuth: { authenticate: mockAuthenticateWindchill },
53+
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
54+
vi.mock('@/lib/auth/internal-delegation', () => ({
55+
bindInternalExecutorDelegation: mockBindDelegation,
56+
InvalidInternalDelegationBindingError: MockInvalidBindingError,
5157
}))
58+
vi.unmock('@/lib/auth/internal')
5259

5360
vi.mock('@/app/api/files/authorization', () => ({
5461
assertToolFileAccess: mockAssertToolFileAccess,
@@ -79,6 +86,7 @@ vi.mock('@/tools/windchill/utils.server', () => ({
7986
WindchillProviderError: MockWindchillProviderError,
8087
}))
8188

89+
import { generateInternalDelegationToken, generateInternalToken } from '@/lib/auth/internal'
8290
import { POST } from '@/app/api/tools/windchill/route'
8391

8492
const BASE_BODY = {
@@ -89,6 +97,15 @@ const BASE_BODY = {
8997

9098
const DOCUMENT_OID = 'OR:wt.doc.WTDocument:1'
9199
const SECOND_DOCUMENT_OID = 'OR:wt.doc.WTDocument:2'
100+
let delegationToken = ''
101+
let legacyInternalToken = ''
102+
103+
function createMockRequest(method: string, body: unknown, headers: Record<string, string> = {}) {
104+
return createTestingRequest(method, body, {
105+
authorization: `Bearer ${delegationToken}`,
106+
...headers,
107+
})
108+
}
92109

93110
const MUTATION_CASES = [
94111
{
@@ -237,22 +254,34 @@ const MUTATION_PAYLOAD_CASES = [
237254
},
238255
] as const
239256

257+
beforeAll(async () => {
258+
delegationToken = await generateInternalDelegationToken({
259+
subjectUserId: 'user-1',
260+
workflowId: '550e8400-e29b-41d4-a716-446655440001',
261+
})
262+
legacyInternalToken = await generateInternalToken()
263+
})
264+
265+
afterAll(resetEnvMock)
266+
240267
beforeEach(() => {
241268
vi.clearAllMocks()
242-
mockAuthenticateWindchill.mockResolvedValue({
269+
mockGetSession.mockResolvedValue(null)
270+
mockBindDelegation.mockImplementation(async (delegation, options) => ({
243271
kind: 'delegated',
244272
serviceId: 'executor',
245-
subjectUserId: 'user-1',
273+
subjectUserId: delegation.subjectUserId,
246274
workspaceId: '550e8400-e29b-41d4-a716-446655440000',
247-
delegationId: 'delegation-1',
248-
audience: 'sim:windchill',
249-
issuedAt: new Date('2026-01-01T00:00:00.000Z'),
250-
expiresAt: new Date('2027-01-01T00:00:00.000Z'),
275+
delegationId: delegation.delegationId,
276+
audience: options.audience,
277+
issuedAt: delegation.issuedAt,
278+
expiresAt: delegation.expiresAt,
251279
delegationContext: {
252280
kind: 'workflow_execution',
253-
workflowId: '550e8400-e29b-41d4-a716-446655440001',
281+
workflowId: delegation.workflowId,
282+
executionId: delegation.executionId,
254283
},
255-
})
284+
}))
256285
mockCreateWindchillSession.mockResolvedValue({
257286
nonceHeader: 'CSRF_NONCE',
258287
nonceValue: 'nonce-value',
@@ -290,17 +319,48 @@ beforeEach(() => {
290319

291320
describe('POST /api/tools/windchill', () => {
292321
it('authenticates before parsing the request body', async () => {
293-
mockAuthenticateWindchill.mockRejectedValueOnce(
294-
new InternalUnauthenticatedError('Authentication required')
295-
)
296-
297-
const response = await POST(createMockRequest('POST', { operation: 'not-valid' }))
322+
const response = await POST(createTestingRequest('POST', { operation: 'not-valid' }))
298323

299324
expect(response.status).toBe(401)
300-
expect(await response.json()).toEqual({ success: false, error: 'Authentication required' })
325+
expect(await response.json()).toEqual({ success: false, error: 'Unauthorized' })
301326
expect(mockCreateWindchillSession).not.toHaveBeenCalled()
302327
})
303328

329+
it('binds executor identity and scope through the canonical delegation path', async () => {
330+
const response = await POST(
331+
createMockRequest('POST', {
332+
...BASE_BODY,
333+
operation: 'windchill_update_document',
334+
documentOid: DOCUMENT_OID,
335+
attributes: { Title: 'Updated' },
336+
})
337+
)
338+
339+
expect(response.status).toBe(200)
340+
expect(mockBindDelegation).toHaveBeenCalledWith(expect.any(Object), {
341+
audience: 'sim:windchill',
342+
resourceScope: undefined,
343+
})
344+
})
345+
346+
it('rejects browser sessions and legacy internal tokens', async () => {
347+
mockGetSession.mockResolvedValueOnce({
348+
user: { id: 'user-1' },
349+
session: { id: 'session-1' },
350+
})
351+
352+
const sessionResponse = await POST(createTestingRequest('POST', BASE_BODY))
353+
const legacyResponse = await POST(
354+
createTestingRequest('POST', BASE_BODY, {
355+
authorization: `Bearer ${legacyInternalToken}`,
356+
})
357+
)
358+
359+
expect(sessionResponse.status).toBe(401)
360+
expect(legacyResponse.status).toBe(401)
361+
expect(mockBindDelegation).not.toHaveBeenCalled()
362+
})
363+
304364
it('rejects malformed operation inputs at the shared contract boundary', async () => {
305365
const response = await POST(
306366
createMockRequest('POST', {
@@ -670,7 +730,7 @@ describe('POST /api/tools/windchill', () => {
670730
})
671731

672732
it('uses execution storage derived from the bound delegation principal', async () => {
673-
mockAuthenticateWindchill.mockResolvedValueOnce({
733+
mockBindDelegation.mockResolvedValueOnce({
674734
kind: 'delegated',
675735
serviceId: 'executor',
676736
subjectUserId: 'user-1',

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import type {
88
} from '@/lib/api/contracts/tools/windchill'
99
import { windchillOperationContract } from '@/lib/api/contracts/tools/windchill'
1010
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
11-
import { InternalUnauthenticatedError } from '@/lib/api/server/routes'
11+
import {
12+
createInternalSessionOrExecutorAuth,
13+
InternalUnauthenticatedError,
14+
} from '@/lib/api/server/routes'
1215
import { generateRequestId } from '@/lib/core/utils/request'
1316
import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
1417
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -19,7 +22,6 @@ import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
1922
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
2023
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
2124
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
22-
import { internalWindchillExecutorAuth } from '@/lib/windchill/api/route-policies'
2325
import { assertToolFileAccess } from '@/app/api/files/authorization'
2426
import { sanitizeFileName } from '@/executor/constants'
2527
import type { UserFile } from '@/executor/types'
@@ -44,6 +46,23 @@ export const dynamic = 'force-dynamic'
4446
export const maxDuration = 900
4547

4648
const logger = createLogger('WindchillAPI')
49+
const windchillSessionOrExecutorAuth = createInternalSessionOrExecutorAuth({
50+
audience: 'sim:windchill',
51+
})
52+
53+
async function authenticateWindchillExecutor(
54+
request: NextRequest
55+
): Promise<WorkflowExecutionDelegatedPrincipal> {
56+
const principal = await windchillSessionOrExecutorAuth.authenticate(request, {})
57+
if (
58+
principal.kind !== 'delegated' ||
59+
principal.serviceId !== 'executor' ||
60+
!('delegationContext' in principal)
61+
) {
62+
throw new InternalUnauthenticatedError('Authentication required')
63+
}
64+
return principal
65+
}
4766

4867
type WindchillRouteOutput = Extract<WindchillOperationResponse, { success: true }>['output']
4968
type MutationOperation = Exclude<
@@ -522,7 +541,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
522541
const requestId = generateRequestId()
523542
let principal: WorkflowExecutionDelegatedPrincipal
524543
try {
525-
principal = await internalWindchillExecutorAuth.authenticate(request, {})
544+
principal = await authenticateWindchillExecutor(request)
526545
} catch (error) {
527546
if (error instanceof InternalUnauthenticatedError) {
528547
return failureResponse(error.message, 401)

apps/sim/lib/windchill/api/route-policies.test.ts

Lines changed: 0 additions & 106 deletions
This file was deleted.

apps/sim/lib/windchill/api/route-policies.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)