Skip to content

Commit c00b185

Browse files
fix(webhook-modal): on copy do not change webhook url, fix auth to use regular perms system (#812)
* fix(webhook-modal): don't regenerate webhook url on copy * fix authentication checks for webhook saves
1 parent 95efae9 commit c00b185

File tree

3 files changed

+135
-20
lines changed
  • apps/sim/app

3 files changed

+135
-20
lines changed

apps/sim/app/api/webhooks/[id]/route.ts

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { and, eq } from 'drizzle-orm'
1+
import { eq } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { getSession } from '@/lib/auth'
44
import { createLogger } from '@/lib/logs/console/logger'
5+
import { getUserEntityPermissions } from '@/lib/permissions/utils'
56
import { db } from '@/db'
67
import { webhook, workflow } from '@/db/schema'
78

@@ -29,18 +30,47 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
2930
workflow: {
3031
id: workflow.id,
3132
name: workflow.name,
33+
userId: workflow.userId,
34+
workspaceId: workflow.workspaceId,
3235
},
3336
})
3437
.from(webhook)
3538
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
36-
.where(and(eq(webhook.id, id), eq(workflow.userId, session.user.id)))
39+
.where(eq(webhook.id, id))
3740
.limit(1)
3841

3942
if (webhooks.length === 0) {
4043
logger.warn(`[${requestId}] Webhook not found: ${id}`)
4144
return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
4245
}
4346

47+
const webhookData = webhooks[0]
48+
49+
// Check if user has permission to access this webhook
50+
let hasAccess = false
51+
52+
// Case 1: User owns the workflow
53+
if (webhookData.workflow.userId === session.user.id) {
54+
hasAccess = true
55+
}
56+
57+
// Case 2: Workflow belongs to a workspace and user has any permission
58+
if (!hasAccess && webhookData.workflow.workspaceId) {
59+
const userPermission = await getUserEntityPermissions(
60+
session.user.id,
61+
'workspace',
62+
webhookData.workflow.workspaceId
63+
)
64+
if (userPermission !== null) {
65+
hasAccess = true
66+
}
67+
}
68+
69+
if (!hasAccess) {
70+
logger.warn(`[${requestId}] User ${session.user.id} denied access to webhook: ${id}`)
71+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
72+
}
73+
4474
logger.info(`[${requestId}] Successfully retrieved webhook: ${id}`)
4575
return NextResponse.json({ webhook: webhooks[0] }, { status: 200 })
4676
} catch (error) {
@@ -66,13 +96,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
6696
const body = await request.json()
6797
const { path, provider, providerConfig, isActive } = body
6898

69-
// Find the webhook and check ownership
99+
// Find the webhook and check permissions
70100
const webhooks = await db
71101
.select({
72102
webhook: webhook,
73103
workflow: {
74104
id: workflow.id,
75105
userId: workflow.userId,
106+
workspaceId: workflow.workspaceId,
76107
},
77108
})
78109
.from(webhook)
@@ -85,9 +116,33 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
85116
return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
86117
}
87118

88-
if (webhooks[0].workflow.userId !== session.user.id) {
89-
logger.warn(`[${requestId}] Unauthorized webhook update attempt for webhook: ${id}`)
90-
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
119+
const webhookData = webhooks[0]
120+
121+
// Check if user has permission to modify this webhook
122+
let canModify = false
123+
124+
// Case 1: User owns the workflow
125+
if (webhookData.workflow.userId === session.user.id) {
126+
canModify = true
127+
}
128+
129+
// Case 2: Workflow belongs to a workspace and user has write or admin permission
130+
if (!canModify && webhookData.workflow.workspaceId) {
131+
const userPermission = await getUserEntityPermissions(
132+
session.user.id,
133+
'workspace',
134+
webhookData.workflow.workspaceId
135+
)
136+
if (userPermission === 'write' || userPermission === 'admin') {
137+
canModify = true
138+
}
139+
}
140+
141+
if (!canModify) {
142+
logger.warn(
143+
`[${requestId}] User ${session.user.id} denied permission to modify webhook: ${id}`
144+
)
145+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
91146
}
92147

93148
logger.debug(`[${requestId}] Updating webhook properties`, {
@@ -136,13 +191,14 @@ export async function DELETE(
136191
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
137192
}
138193

139-
// Find the webhook and check ownership
194+
// Find the webhook and check permissions
140195
const webhooks = await db
141196
.select({
142197
webhook: webhook,
143198
workflow: {
144199
id: workflow.id,
145200
userId: workflow.userId,
201+
workspaceId: workflow.workspaceId,
146202
},
147203
})
148204
.from(webhook)
@@ -155,12 +211,36 @@ export async function DELETE(
155211
return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
156212
}
157213

158-
if (webhooks[0].workflow.userId !== session.user.id) {
159-
logger.warn(`[${requestId}] Unauthorized webhook deletion attempt for webhook: ${id}`)
160-
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
214+
const webhookData = webhooks[0]
215+
216+
// Check if user has permission to delete this webhook
217+
let canDelete = false
218+
219+
// Case 1: User owns the workflow
220+
if (webhookData.workflow.userId === session.user.id) {
221+
canDelete = true
222+
}
223+
224+
// Case 2: Workflow belongs to a workspace and user has write or admin permission
225+
if (!canDelete && webhookData.workflow.workspaceId) {
226+
const userPermission = await getUserEntityPermissions(
227+
session.user.id,
228+
'workspace',
229+
webhookData.workflow.workspaceId
230+
)
231+
if (userPermission === 'write' || userPermission === 'admin') {
232+
canDelete = true
233+
}
234+
}
235+
236+
if (!canDelete) {
237+
logger.warn(
238+
`[${requestId}] User ${session.user.id} denied permission to delete webhook: ${id}`
239+
)
240+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
161241
}
162242

163-
const foundWebhook = webhooks[0].webhook
243+
const foundWebhook = webhookData.webhook
164244

165245
// If it's a Telegram webhook, delete it from Telegram first
166246
if (foundWebhook.provider === 'telegram') {

apps/sim/app/api/webhooks/route.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { getSession } from '@/lib/auth'
55
import { env } from '@/lib/env'
66
import { createLogger } from '@/lib/logs/console/logger'
7+
import { getUserEntityPermissions } from '@/lib/permissions/utils'
78
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
89
import { db } from '@/db'
910
import { webhook, workflow } from '@/db/schema'
@@ -94,18 +95,51 @@ export async function POST(request: NextRequest) {
9495
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
9596
}
9697

97-
// Check if the workflow belongs to the user
98-
const workflows = await db
99-
.select({ id: workflow.id }) // Select only necessary field
98+
// Check if the workflow exists and user has permission to modify it
99+
const workflowData = await db
100+
.select({
101+
id: workflow.id,
102+
userId: workflow.userId,
103+
workspaceId: workflow.workspaceId,
104+
})
100105
.from(workflow)
101-
.where(and(eq(workflow.id, workflowId), eq(workflow.userId, userId)))
106+
.where(eq(workflow.id, workflowId))
102107
.limit(1)
103108

104-
if (workflows.length === 0) {
105-
logger.warn(`[${requestId}] Workflow not found or not owned by user: ${workflowId}`)
109+
if (workflowData.length === 0) {
110+
logger.warn(`[${requestId}] Workflow not found: ${workflowId}`)
106111
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
107112
}
108113

114+
const workflowRecord = workflowData[0]
115+
116+
// Check if user has permission to modify this workflow
117+
let canModify = false
118+
119+
// Case 1: User owns the workflow
120+
if (workflowRecord.userId === userId) {
121+
canModify = true
122+
}
123+
124+
// Case 2: Workflow belongs to a workspace and user has write or admin permission
125+
if (!canModify && workflowRecord.workspaceId) {
126+
const userPermission = await getUserEntityPermissions(
127+
userId,
128+
'workspace',
129+
workflowRecord.workspaceId
130+
)
131+
if (userPermission === 'write' || userPermission === 'admin') {
132+
canModify = true
133+
}
134+
}
135+
136+
if (!canModify) {
137+
logger.warn(
138+
`[${requestId}] User ${userId} denied permission to modify webhook for workflow ${workflowId}`
139+
)
140+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
141+
}
142+
109143
// Check if a webhook with the same path already exists
110144
const existingWebhooks = await db
111145
.select({ id: webhook.id, workflowId: webhook.workflowId })

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react'
1+
import { useEffect, useMemo, useState } from 'react'
22
import { X } from 'lucide-react'
33
import { Button } from '@/components/ui/button'
44
import {
@@ -392,8 +392,9 @@ export function WebhookModal({
392392
microsoftTeamsHmacSecret,
393393
])
394394

395-
// Use the provided path or generate a UUID-based path
396-
const formattedPath = webhookPath && webhookPath.trim() !== '' ? webhookPath : crypto.randomUUID()
395+
const formattedPath = useMemo(() => {
396+
return webhookPath && webhookPath.trim() !== '' ? webhookPath : crypto.randomUUID()
397+
}, [webhookPath])
397398

398399
// Construct the full webhook URL
399400
const baseUrl =

0 commit comments

Comments
 (0)