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>
195 lines
5.0 KiB
TypeScript
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,
|
|
})),
|
|
};
|
|
}
|
|
}
|