Skip to content

Commit a10e1a6

Browse files
authored
fix(permissions): add client-side permissions validation to prevent unauthorized actions, upgraded custom tool modal (#2130)
* fix(permissions): add client-side permissions validation to prevent unauthorized actions, upgraded custom tool modal * fix failing test * fix test * cleanup
1 parent be91cd3 commit a10e1a6

File tree

10 files changed

+181
-216
lines changed

10 files changed

+181
-216
lines changed

apps/docs/components/icons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4330,7 +4330,7 @@ export function PylonIcon(props: SVGProps<SVGSVGElement>) {
43304330
viewBox='0 0 26 26'
43314331
fill='none'
43324332
>
4333-
<g clip-path='url(#clip0_6559_17753)'>
4333+
<g clipPath='url(#clip0_6559_17753)'>
43344334
<path
43354335
d='M21.3437 4.1562C18.9827 1.79763 15.8424 0.5 12.5015 0.5C9.16056 0.5 6.02027 1.79763 3.66091 4.15455C1.29989 6.51147 0 9.64465 0 12.9798C0 16.3149 1.29989 19.448 3.66091 21.805C6.02193 24.1619 9.16222 25.4612 12.5031 25.4612C15.844 25.4612 18.9843 24.1635 21.3454 21.8066C23.7064 19.4497 25.0063 16.3165 25.0063 12.9814C25.0063 9.6463 23.7064 6.51312 21.3454 4.1562H21.3437ZM22.3949 12.9814C22.3949 17.927 18.7074 22.1227 13.8063 22.7699V3.1896C18.7074 3.83676 22.3949 8.0342 22.3949 12.9798V12.9814ZM4.8265 6.75643C6.43312 4.7835 8.68803 3.52063 11.1983 3.1896V6.75643H4.8265ZM11.1983 9.36162V11.6904H2.69428C2.79874 10.8926 3.00267 10.1097 3.2978 9.36162H11.1983ZM11.1983 14.2939V16.6227H3.30775C3.00931 15.8746 2.80371 15.0917 2.6976 14.2939H11.1983ZM11.1983 19.2279V22.7699C8.70129 22.4405 6.45302 21.1859 4.84805 19.2279H11.1983Z'
43364336
fill='#5B0EFF'

apps/docs/content/docs/en/tools/zendesk.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ With Zendesk in Sim, you can:
4242
By leveraging Zendesk’s Sim integration, your automated workflows can seamlessly handle support ticket triage, user onboarding/offboarding, company management, and keep your support operations running smoothly. Whether you’re integrating support with product, CRM, or automation systems, Zendesk tools in Sim provide robust, programmatic control to power best-in-class support at scale.
4343
{/* MANUAL-CONTENT-END */}
4444

45+
4546
## Usage Instructions
4647

4748
Integrate Zendesk into the workflow. Can get tickets, get ticket, create ticket, create tickets bulk, update ticket, update tickets bulk, delete ticket, merge tickets, get users, get user, get current user, search users, create user, create users bulk, update user, update users bulk, delete user, get organizations, get organization, autocomplete organizations, create organization, create organizations bulk, update organization, delete organization, search, search count.

apps/sim/app/api/chat/manage/[id]/route.test.ts

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ describe('Chat Edit API Route', () => {
1919
const mockCreateErrorResponse = vi.fn()
2020
const mockEncryptSecret = vi.fn()
2121
const mockCheckChatAccess = vi.fn()
22-
const mockGetSession = vi.fn()
22+
const mockDeployWorkflow = vi.fn()
2323

2424
beforeEach(() => {
2525
vi.resetModules()
2626

27+
// Set default return values
28+
mockLimit.mockResolvedValue([])
2729
mockSelect.mockReturnValue({ from: mockFrom })
2830
mockFrom.mockReturnValue({ where: mockWhere })
2931
mockWhere.mockReturnValue({ limit: mockLimit })
@@ -43,10 +45,6 @@ describe('Chat Edit API Route', () => {
4345
chat: { id: 'id', identifier: 'identifier', userId: 'userId' },
4446
}))
4547

46-
vi.doMock('@/lib/auth', () => ({
47-
getSession: mockGetSession,
48-
}))
49-
5048
vi.doMock('@/lib/logs/console/logger', () => ({
5149
createLogger: vi.fn().mockReturnValue({
5250
info: vi.fn(),
@@ -86,6 +84,15 @@ describe('Chat Edit API Route', () => {
8684
vi.doMock('@/app/api/chat/utils', () => ({
8785
checkChatAccess: mockCheckChatAccess,
8886
}))
87+
88+
mockDeployWorkflow.mockResolvedValue({ success: true, version: 1 })
89+
vi.doMock('@/lib/workflows/db-helpers', () => ({
90+
deployWorkflow: mockDeployWorkflow,
91+
}))
92+
93+
vi.doMock('drizzle-orm', () => ({
94+
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
95+
}))
8996
})
9097

9198
afterEach(() => {
@@ -94,20 +101,25 @@ describe('Chat Edit API Route', () => {
94101

95102
describe('GET', () => {
96103
it('should return 401 when user is not authenticated', async () => {
97-
mockGetSession.mockResolvedValueOnce(null)
104+
vi.doMock('@/lib/auth', () => ({
105+
getSession: vi.fn().mockResolvedValue(null),
106+
}))
98107

99108
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123')
100109
const { GET } = await import('@/app/api/chat/manage/[id]/route')
101110
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
102111

103112
expect(response.status).toBe(401)
104-
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
113+
const data = await response.json()
114+
expect(data.error).toBe('Unauthorized')
105115
})
106116

107117
it('should return 404 when chat not found or access denied', async () => {
108-
mockGetSession.mockResolvedValueOnce({
109-
user: { id: 'user-id' },
110-
})
118+
vi.doMock('@/lib/auth', () => ({
119+
getSession: vi.fn().mockResolvedValue({
120+
user: { id: 'user-id' },
121+
}),
122+
}))
111123

112124
mockCheckChatAccess.mockResolvedValue({ hasAccess: false })
113125

@@ -116,7 +128,8 @@ describe('Chat Edit API Route', () => {
116128
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
117129

118130
expect(response.status).toBe(404)
119-
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
131+
const data = await response.json()
132+
expect(data.error).toBe('Chat not found or access denied')
120133
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
121134
})
122135

@@ -143,15 +156,12 @@ describe('Chat Edit API Route', () => {
143156
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
144157

145158
expect(response.status).toBe(200)
146-
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
147-
id: 'chat-123',
148-
identifier: 'test-chat',
149-
title: 'Test Chat',
150-
description: 'A test chat',
151-
customizations: { primaryColor: '#000000' },
152-
chatUrl: 'http://localhost:3000/chat/test-chat',
153-
hasPassword: true,
154-
})
159+
const data = await response.json()
160+
expect(data.id).toBe('chat-123')
161+
expect(data.identifier).toBe('test-chat')
162+
expect(data.title).toBe('Test Chat')
163+
expect(data.chatUrl).toBe('http://localhost:3000/chat/test-chat')
164+
expect(data.hasPassword).toBe(true)
155165
})
156166
})
157167

@@ -169,7 +179,8 @@ describe('Chat Edit API Route', () => {
169179
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
170180

171181
expect(response.status).toBe(401)
172-
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
182+
const data = await response.json()
183+
expect(data.error).toBe('Unauthorized')
173184
})
174185

175186
it('should return 404 when chat not found or access denied', async () => {
@@ -189,7 +200,8 @@ describe('Chat Edit API Route', () => {
189200
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
190201

191202
expect(response.status).toBe(404)
192-
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
203+
const data = await response.json()
204+
expect(data.error).toBe('Chat not found or access denied')
193205
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
194206
})
195207

@@ -205,9 +217,11 @@ describe('Chat Edit API Route', () => {
205217
identifier: 'test-chat',
206218
title: 'Test Chat',
207219
authType: 'public',
220+
workflowId: 'workflow-123',
208221
}
209222

210223
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
224+
mockLimit.mockResolvedValueOnce([]) // No identifier conflict
211225

212226
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
213227
method: 'PATCH',
@@ -218,11 +232,10 @@ describe('Chat Edit API Route', () => {
218232

219233
expect(response.status).toBe(200)
220234
expect(mockUpdate).toHaveBeenCalled()
221-
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
222-
id: 'chat-123',
223-
chatUrl: 'http://localhost:3000/chat/test-chat',
224-
message: 'Chat deployment updated successfully',
225-
})
235+
const data = await response.json()
236+
expect(data.id).toBe('chat-123')
237+
expect(data.chatUrl).toBe('http://localhost:3000/chat/test-chat')
238+
expect(data.message).toBe('Chat deployment updated successfully')
226239
})
227240

228241
it('should handle identifier conflicts', async () => {
@@ -236,11 +249,15 @@ describe('Chat Edit API Route', () => {
236249
id: 'chat-123',
237250
identifier: 'test-chat',
238251
title: 'Test Chat',
252+
workflowId: 'workflow-123',
239253
}
240254

241255
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
242-
// Mock identifier conflict
243-
mockLimit.mockResolvedValueOnce([{ id: 'other-chat-id', identifier: 'new-identifier' }])
256+
257+
// Reset and reconfigure mockLimit to return the conflict
258+
mockLimit.mockReset()
259+
mockLimit.mockResolvedValue([{ id: 'other-chat-id', identifier: 'new-identifier' }])
260+
mockWhere.mockReturnValue({ limit: mockLimit })
244261

245262
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
246263
method: 'PATCH',
@@ -250,7 +267,8 @@ describe('Chat Edit API Route', () => {
250267
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
251268

252269
expect(response.status).toBe(400)
253-
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Identifier already in use', 400)
270+
const data = await response.json()
271+
expect(data.error).toBe('Identifier already in use')
254272
})
255273

256274
it('should validate password requirement for password auth', async () => {
@@ -266,6 +284,7 @@ describe('Chat Edit API Route', () => {
266284
title: 'Test Chat',
267285
authType: 'public',
268286
password: null,
287+
workflowId: 'workflow-123',
269288
}
270289

271290
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
@@ -278,10 +297,8 @@ describe('Chat Edit API Route', () => {
278297
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
279298

280299
expect(response.status).toBe(400)
281-
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
282-
'Password is required when using password protection',
283-
400
284-
)
300+
const data = await response.json()
301+
expect(data.error).toBe('Password is required when using password protection')
285302
})
286303

287304
it('should allow access when user has workspace admin permission', async () => {
@@ -296,10 +313,12 @@ describe('Chat Edit API Route', () => {
296313
identifier: 'test-chat',
297314
title: 'Test Chat',
298315
authType: 'public',
316+
workflowId: 'workflow-123',
299317
}
300318

301319
// User doesn't own chat but has workspace admin access
302320
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
321+
mockLimit.mockResolvedValueOnce([]) // No identifier conflict
303322

304323
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
305324
method: 'PATCH',
@@ -326,7 +345,8 @@ describe('Chat Edit API Route', () => {
326345
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
327346

328347
expect(response.status).toBe(401)
329-
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
348+
const data = await response.json()
349+
expect(data.error).toBe('Unauthorized')
330350
})
331351

332352
it('should return 404 when chat not found or access denied', async () => {
@@ -345,7 +365,8 @@ describe('Chat Edit API Route', () => {
345365
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
346366

347367
expect(response.status).toBe(404)
348-
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
368+
const data = await response.json()
369+
expect(data.error).toBe('Chat not found or access denied')
349370
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
350371
})
351372

@@ -367,9 +388,8 @@ describe('Chat Edit API Route', () => {
367388

368389
expect(response.status).toBe(200)
369390
expect(mockDelete).toHaveBeenCalled()
370-
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
371-
message: 'Chat deployment deleted successfully',
372-
})
391+
const data = await response.json()
392+
expect(data.message).toBe('Chat deployment deleted successfully')
373393
})
374394

375395
it('should allow deletion when user has workspace admin permission', async () => {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ export async function POST(req: NextRequest) {
175175
}
176176
} catch (error) {
177177
logger.error(`[${requestId}] Error updating custom tools`, error)
178-
return NextResponse.json({ error: 'Failed to update custom tools' }, { status: 500 })
178+
const errorMessage = error instanceof Error ? error.message : 'Failed to update custom tools'
179+
return NextResponse.json({ error: errorMessage }, { status: 500 })
179180
}
180181
}
181182

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx

Lines changed: 20 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,9 @@ export function CodeEditor({
9797
return () => resizeObserver.disconnect()
9898
}, [code])
9999

100-
// Calculate the number of lines to determine gutter width
101100
const lineCount = code.split('\n').length
102101
const gutterWidth = calculateGutterWidth(lineCount)
103102

104-
// Render helpers
105103
const renderLineNumbers = () => {
106104
const numbers: ReactElement[] = []
107105
let lineNumber = 1
@@ -127,88 +125,41 @@ export function CodeEditor({
127125
return numbers
128126
}
129127

130-
// Custom highlighter that highlights environment variables and tags
131128
const customHighlight = (code: string) => {
132129
if (!highlightVariables || language !== 'javascript') {
133-
// Use default Prism highlighting for non-JS or when variable highlighting is off
134130
return highlight(code, languages[language], language)
135131
}
136132

137-
// First, get the default Prism highlighting
138-
let highlighted = highlight(code, languages[language], language)
133+
const placeholders: Array<{ placeholder: string; original: string; type: 'env' | 'param' }> = []
134+
let processedCode = code
139135

140-
// Collect all syntax highlights to apply in a single pass
141-
type SyntaxHighlight = {
142-
start: number
143-
end: number
144-
replacement: string
145-
}
146-
const highlights: SyntaxHighlight[] = []
147-
148-
// Find environment variables with {{var_name}} syntax
149-
let match
150-
const envVarRegex = /\{\{([^}]+)\}\}/g
151-
while ((match = envVarRegex.exec(highlighted)) !== null) {
152-
highlights.push({
153-
start: match.index,
154-
end: match.index + match[0].length,
155-
replacement: `<span class="text-[#34B5FF] dark:text-[#34B5FF]">${match[0]}</span>`,
156-
})
157-
}
158-
159-
// Find tags with <tag_name> syntax (not in HTML context)
160-
if (!language.includes('html')) {
161-
const tagRegex = /<([^>\s/]+)>/g
162-
while ((match = tagRegex.exec(highlighted)) !== null) {
163-
// Skip HTML comments and closing tags
164-
if (!match[0].startsWith('<!--') && !match[0].includes('</')) {
165-
const escaped = `&lt;${match[1]}&gt;`
166-
highlights.push({
167-
start: match.index,
168-
end: match.index + match[0].length,
169-
replacement: `<span class="text-[#34B5FF] dark:text-[#34B5FF]">${escaped}</span>`,
170-
})
171-
}
172-
}
173-
}
136+
processedCode = processedCode.replace(/\{\{([^}]+)\}\}/g, (match) => {
137+
const placeholder = `__ENV_VAR_${placeholders.length}__`
138+
placeholders.push({ placeholder, original: match, type: 'env' })
139+
return placeholder
140+
})
174141

175-
// Find schema parameters as whole words
176142
if (schemaParameters.length > 0) {
177143
schemaParameters.forEach((param) => {
178-
// Escape special regex characters in parameter name
179144
const escapedName = param.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
180145
const paramRegex = new RegExp(`\\b(${escapedName})\\b`, 'g')
181-
while ((match = paramRegex.exec(highlighted)) !== null) {
182-
// Check if this position is already inside an HTML tag
183-
// by looking for unclosed < before this position
184-
let insideTag = false
185-
let pos = match.index - 1
186-
while (pos >= 0) {
187-
if (highlighted[pos] === '>') break
188-
if (highlighted[pos] === '<') {
189-
insideTag = true
190-
break
191-
}
192-
pos--
193-
}
194-
195-
if (!insideTag) {
196-
highlights.push({
197-
start: match.index,
198-
end: match.index + match[0].length,
199-
replacement: `<span class="text-[#34B5FF] dark:text-[#34B5FF] font-medium">${match[0]}</span>`,
200-
})
201-
}
202-
}
146+
processedCode = processedCode.replace(paramRegex, (match) => {
147+
const placeholder = `__PARAM_${placeholders.length}__`
148+
placeholders.push({ placeholder, original: match, type: 'param' })
149+
return placeholder
150+
})
203151
})
204152
}
205153

206-
// Sort highlights by start position (reverse order to maintain positions)
207-
highlights.sort((a, b) => b.start - a.start)
154+
let highlighted = highlight(processedCode, languages[language], language)
155+
156+
placeholders.forEach(({ placeholder, original, type }) => {
157+
const replacement =
158+
type === 'env'
159+
? `<span style="color: #34B5FF;">${original}</span>`
160+
: `<span style="color: #34B5FF; font-weight: 500;">${original}</span>`
208161

209-
// Apply all highlights
210-
highlights.forEach(({ start, end, replacement }) => {
211-
highlighted = highlighted.slice(0, start) + replacement + highlighted.slice(end)
162+
highlighted = highlighted.replace(placeholder, replacement)
212163
})
213164

214165
return highlighted

0 commit comments

Comments
 (0)