Files
calendar/packages/trpc/server/routers/viewer/webhook/util.ts
T
sean-brydonGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
a44a3e5b84 feat: add Webhook resource to PBAC system with permission enforcement (#23614)
* feat: add Webhook resource to PBAC system with permission enforcement

- Add Webhook resource to PBAC permission registry with CRUD actions
- Implement PBAC permission checks in webhook handlers (create, edit, delete)
- Add webhook permission translations to common.json
- Use PermissionCheckService with fallback roles [ADMIN, OWNER] for team webhooks
- Maintain backward compatibility when PBAC is disabled
- Follow same pattern as workflow PBAC implementation from PR #22845

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* fix: implement PBAC permission filtering in webhook list handler

- Add PermissionCheckService to filter team webhooks by webhook.read permission
- Only show webhooks from teams where user has proper permissions
- Maintain backward compatibility with fallback to all team memberships

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* add migration for default roles

* new forUserMethod

* update webhook repository

* fix UI showing/hiding webhooks for webhoo.create teams

* WIP pbac procedure migratoin + tests

* add more roles to get fallback

* permissions in cmponents instead of readOnly

* passPermissions to list item

* push instant events logic

* Git merge

* wip teamId accessable refactor

* fix delete handler

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-18 07:37:45 -03:00

170 lines
5.5 KiB
TypeScript

import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { prisma } from "@calcom/prisma";
import type { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import authedProcedure from "../../../procedures/authedProcedure";
import { webhookIdAndEventTypeIdSchema } from "./types";
/**
* Creates a webhook procedure with configurable PBAC permissions
* @param permission - The specific permission required (e.g., "webhook.create", "webhook.update")
* @param fallbackRoles - Roles to check when PBAC is disabled (defaults to ["ADMIN", "OWNER"])
* @returns A procedure that checks the specified permission
*/
export const createWebhookPbacProcedure = (
permission: PermissionString,
fallbackRoles: MembershipRole[] = ["ADMIN", "OWNER"]
) => {
return authedProcedure.input(webhookIdAndEventTypeIdSchema.optional()).use(async ({ ctx, input, next }) => {
// Endpoints that just read the logged in user's data - like 'list' don't necessarily have any input
if (!input) return next();
const { id, teamId, eventTypeId } = input;
const permissionCheckService = new PermissionCheckService();
if (id) {
// Check if user is authorized to edit webhook
const webhook = await prisma.webhook.findUnique({
where: { id },
select: {
id: true,
userId: true,
teamId: true,
eventTypeId: true,
},
});
if (!webhook) {
throw new TRPCError({ code: "NOT_FOUND" });
}
// Validate consistency
if (teamId && teamId !== webhook.teamId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if (eventTypeId && eventTypeId !== webhook.eventTypeId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// For team webhooks, check PBAC permissions
if (webhook.teamId) {
const hasPermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: webhook.teamId,
permission,
fallbackRoles,
});
if (!hasPermission) {
throw new TRPCError({
code: "FORBIDDEN",
message: `Permission required: ${permission}`,
});
}
} else if (webhook.eventTypeId) {
// For event type webhooks, check if the user owns the event type or has team permissions
const eventType = await prisma.eventType.findUnique({
where: { id: webhook.eventTypeId },
include: { team: true },
});
if (!eventType) {
throw new TRPCError({ code: "NOT_FOUND" });
}
if (eventType.userId !== ctx.user.id) {
// Check team permissions if it's a team event type
if (eventType.teamId) {
const hasPermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: eventType.teamId,
permission,
fallbackRoles,
});
if (!hasPermission) {
throw new TRPCError({
code: "FORBIDDEN",
message: `Permission required: ${permission}`,
});
}
} else {
throw new TRPCError({
code: "FORBIDDEN",
message: `Permission required: ${permission}`,
});
}
}
} else if (webhook.userId && webhook.userId !== ctx.user.id) {
// For personal webhooks, only the owner can manage
throw new TRPCError({
code: "FORBIDDEN",
message: `Permission required: ${permission}`,
});
}
} else {
// Check if user is authorized to create webhook on event type or team
if (teamId) {
const hasPermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId,
permission,
fallbackRoles,
});
if (!hasPermission) {
throw new TRPCError({
code: "FORBIDDEN",
message: `Permission required: ${permission}`,
});
}
} else if (eventTypeId) {
const eventType = await prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { team: true },
});
if (!eventType) {
throw new TRPCError({ code: "NOT_FOUND" });
}
if (eventType.userId !== ctx.user.id) {
// Check team permissions if it's a team event type
if (eventType.teamId) {
const hasPermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: eventType.teamId,
permission,
fallbackRoles,
});
if (!hasPermission) {
throw new TRPCError({
code: "FORBIDDEN",
message: `Permission required: ${permission}`,
});
}
} else {
throw new TRPCError({
code: "FORBIDDEN",
message: `Permission required: ${permission}`,
});
}
}
}
}
return next();
});
};
/**
* Legacy webhook procedure - uses the new PBAC procedure with webhook.update permission
* This maintains backward compatibility while supporting PBAC
*/
export const webhookProcedure = createWebhookPbacProcedure("webhook.update", ["ADMIN", "OWNER"]);