diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts index 36b9691abf..0e0c546755 100644 --- a/apps/sim/app/api/webhooks/[id]/route.ts +++ b/apps/sim/app/api/webhooks/[id]/route.ts @@ -1,7 +1,8 @@ -import { and, eq } from 'drizzle-orm' +import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' import { db } from '@/db' import { webhook, workflow } from '@/db/schema' @@ -29,11 +30,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ workflow: { id: workflow.id, name: workflow.name, + userId: workflow.userId, + workspaceId: workflow.workspaceId, }, }) .from(webhook) .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) - .where(and(eq(webhook.id, id), eq(workflow.userId, session.user.id))) + .where(eq(webhook.id, id)) .limit(1) if (webhooks.length === 0) { @@ -41,6 +44,33 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) } + const webhookData = webhooks[0] + + // Check if user has permission to access this webhook + let hasAccess = false + + // Case 1: User owns the workflow + if (webhookData.workflow.userId === session.user.id) { + hasAccess = true + } + + // Case 2: Workflow belongs to a workspace and user has any permission + if (!hasAccess && webhookData.workflow.workspaceId) { + const userPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + webhookData.workflow.workspaceId + ) + if (userPermission !== null) { + hasAccess = true + } + } + + if (!hasAccess) { + logger.warn(`[${requestId}] User ${session.user.id} denied access to webhook: ${id}`) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } + logger.info(`[${requestId}] Successfully retrieved webhook: ${id}`) return NextResponse.json({ webhook: webhooks[0] }, { status: 200 }) } catch (error) { @@ -66,13 +96,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< const body = await request.json() const { path, provider, providerConfig, isActive } = body - // Find the webhook and check ownership + // Find the webhook and check permissions const webhooks = await db .select({ webhook: webhook, workflow: { id: workflow.id, userId: workflow.userId, + workspaceId: workflow.workspaceId, }, }) .from(webhook) @@ -85,9 +116,33 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) } - if (webhooks[0].workflow.userId !== session.user.id) { - logger.warn(`[${requestId}] Unauthorized webhook update attempt for webhook: ${id}`) - return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + const webhookData = webhooks[0] + + // Check if user has permission to modify this webhook + let canModify = false + + // Case 1: User owns the workflow + if (webhookData.workflow.userId === session.user.id) { + canModify = true + } + + // Case 2: Workflow belongs to a workspace and user has write or admin permission + if (!canModify && webhookData.workflow.workspaceId) { + const userPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + webhookData.workflow.workspaceId + ) + if (userPermission === 'write' || userPermission === 'admin') { + canModify = true + } + } + + if (!canModify) { + logger.warn( + `[${requestId}] User ${session.user.id} denied permission to modify webhook: ${id}` + ) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } logger.debug(`[${requestId}] Updating webhook properties`, { @@ -136,13 +191,14 @@ export async function DELETE( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - // Find the webhook and check ownership + // Find the webhook and check permissions const webhooks = await db .select({ webhook: webhook, workflow: { id: workflow.id, userId: workflow.userId, + workspaceId: workflow.workspaceId, }, }) .from(webhook) @@ -155,12 +211,36 @@ export async function DELETE( return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) } - if (webhooks[0].workflow.userId !== session.user.id) { - logger.warn(`[${requestId}] Unauthorized webhook deletion attempt for webhook: ${id}`) - return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + const webhookData = webhooks[0] + + // Check if user has permission to delete this webhook + let canDelete = false + + // Case 1: User owns the workflow + if (webhookData.workflow.userId === session.user.id) { + canDelete = true + } + + // Case 2: Workflow belongs to a workspace and user has write or admin permission + if (!canDelete && webhookData.workflow.workspaceId) { + const userPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + webhookData.workflow.workspaceId + ) + if (userPermission === 'write' || userPermission === 'admin') { + canDelete = true + } + } + + if (!canDelete) { + logger.warn( + `[${requestId}] User ${session.user.id} denied permission to delete webhook: ${id}` + ) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } - const foundWebhook = webhooks[0].webhook + const foundWebhook = webhookData.webhook // If it's a Telegram webhook, delete it from Telegram first if (foundWebhook.provider === 'telegram') { diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index a9d646a81f..915f334940 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' import { getOAuthToken } from '@/app/api/auth/oauth/utils' import { db } from '@/db' import { webhook, workflow } from '@/db/schema' @@ -94,18 +95,51 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }) } - // Check if the workflow belongs to the user - const workflows = await db - .select({ id: workflow.id }) // Select only necessary field + // Check if the workflow exists and user has permission to modify it + const workflowData = await db + .select({ + id: workflow.id, + userId: workflow.userId, + workspaceId: workflow.workspaceId, + }) .from(workflow) - .where(and(eq(workflow.id, workflowId), eq(workflow.userId, userId))) + .where(eq(workflow.id, workflowId)) .limit(1) - if (workflows.length === 0) { - logger.warn(`[${requestId}] Workflow not found or not owned by user: ${workflowId}`) + if (workflowData.length === 0) { + logger.warn(`[${requestId}] Workflow not found: ${workflowId}`) return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) } + const workflowRecord = workflowData[0] + + // Check if user has permission to modify this workflow + let canModify = false + + // Case 1: User owns the workflow + if (workflowRecord.userId === userId) { + canModify = true + } + + // Case 2: Workflow belongs to a workspace and user has write or admin permission + if (!canModify && workflowRecord.workspaceId) { + const userPermission = await getUserEntityPermissions( + userId, + 'workspace', + workflowRecord.workspaceId + ) + if (userPermission === 'write' || userPermission === 'admin') { + canModify = true + } + } + + if (!canModify) { + logger.warn( + `[${requestId}] User ${userId} denied permission to modify webhook for workflow ${workflowId}` + ) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } + // Check if a webhook with the same path already exists const existingWebhooks = await db .select({ id: webhook.id, workflowId: webhook.workflowId }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx index 4b5315129e..9ceac49910 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { X } from 'lucide-react' import { Button } from '@/components/ui/button' import { @@ -392,8 +392,9 @@ export function WebhookModal({ microsoftTeamsHmacSecret, ]) - // Use the provided path or generate a UUID-based path - const formattedPath = webhookPath && webhookPath.trim() !== '' ? webhookPath : crypto.randomUUID() + const formattedPath = useMemo(() => { + return webhookPath && webhookPath.trim() !== '' ? webhookPath : crypto.randomUUID() + }, [webhookPath]) // Construct the full webhook URL const baseUrl =