Files
calendar/packages/lib/server/repository/webhook.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

195 lines
5.0 KiB
TypeScript

import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { prisma } from "@calcom/prisma";
import type { Webhook } from "@calcom/prisma/client";
import type { UserPermissionRole } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
type WebhookGroup = {
teamId?: number | null;
profile: {
slug: string | null;
name: string | null;
image?: string;
};
metadata?: {
canModify: boolean;
canDelete: boolean;
};
webhooks: Webhook[];
};
const filterWebhooks = (webhook: Webhook) => {
const appIds = [
"zapier",
"make",
// Add more if needed
];
return !appIds.some((appId: string) => webhook.appId == appId);
};
export class WebhookRepository {
static async findByWebhookId(webhookId?: string) {
return await prisma.webhook.findUniqueOrThrow({
where: {
id: webhookId,
},
select: {
id: true,
subscriberUrl: true,
payloadTemplate: true,
active: true,
eventTriggers: true,
secret: true,
teamId: true,
userId: true,
platform: true,
time: true,
timeUnit: true,
},
});
}
static async getFilteredWebhooksForUser({
userId,
userRole,
}: {
userId: number;
userRole?: UserPermissionRole;
}) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
name: true,
avatarUrl: true,
webhooks: true,
teams: {
where: {
accepted: true,
},
select: {
role: true,
team: {
select: {
id: true,
name: true,
slug: true,
logoUrl: true,
webhooks: true,
},
},
},
},
},
});
if (!user) {
throw new Error("User not found");
}
// Use permission service which handles both PBAC and role-based fallbacks
const permissionService = new PermissionCheckService();
// Build webhook groups with proper permissions
const webhookGroups: WebhookGroup[] = [];
// Add user's personal webhooks
webhookGroups.push({
teamId: null,
profile: {
slug: user.username,
name: user.name,
image: getUserAvatarUrl({ avatarUrl: user.avatarUrl }),
},
webhooks: user.webhooks.filter(filterWebhooks),
metadata: {
canModify: true,
canDelete: true,
},
});
// Check permissions for each team
// The permission service handles PBAC when enabled and falls back to role-based permissions
for (const membership of user.teams) {
const teamId = membership.team.id;
// Check read permission (fallback: MEMBER, ADMIN, OWNER can read)
const canRead = await permissionService.checkPermission({
userId,
teamId,
permission: "webhook.read",
fallbackRoles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
});
if (!canRead) {
// User doesn't have permission to view this team's webhooks
continue;
}
// Check update/delete permissions in parallel (fallback: only ADMIN, OWNER can modify)
const [canUpdate, canDelete] = await Promise.all([
permissionService.checkPermission({
userId,
teamId,
permission: "webhook.update",
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
}),
permissionService.checkPermission({
userId,
teamId,
permission: "webhook.delete",
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
}),
]);
webhookGroups.push({
teamId: membership.team.id,
profile: {
name: membership.team.name,
slug: membership.team.slug || null,
image: getPlaceholderAvatar(membership.team.logoUrl, membership.team.name),
},
webhooks: membership.team.webhooks.filter(filterWebhooks),
metadata: {
canModify: canUpdate,
canDelete,
},
});
}
// Add platform webhooks for admins
if (userRole === "ADMIN") {
const platformWebhooks = await prisma.webhook.findMany({
where: { platform: true },
});
webhookGroups.push({
teamId: null,
profile: {
slug: "Platform",
name: "Platform",
image: getPlaceholderAvatar(null, "Platform"),
},
webhooks: platformWebhooks,
metadata: {
canDelete: true,
canModify: true,
},
});
}
return {
webhookGroups: webhookGroups.filter((group) => group.webhooks.length > 0),
profiles: webhookGroups.map((group) => ({
teamId: group.teamId,
...group.profile,
...group.metadata,
})),
};
}
}