a44a3e5b84
* 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>
170 lines
5.5 KiB
TypeScript
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"]);
|