feat: add Workflow resource to PBAC system with permission enforcement (#22845)

Co-authored-by: sean@cal.com <Sean@brydon.io>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
sean-brydon
2025-08-08 08:48:18 +00:00
committed by GitHub
co-authored by sean@cal.com <Sean@brydon.io> Carina Wollendorfer Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> CarinaWolli
parent d18e23378b
commit 3ea73e75d6
18 changed files with 820 additions and 139 deletions
@@ -1,7 +1,10 @@
import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils.server";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { PrismaClient } from "@calcom/prisma";
import type { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
@@ -89,6 +92,29 @@ export const teamsAndUserProfilesQuery = async ({ ctx, input }: TeamsAndUserProf
}));
}
// Filter teams based on permission if provided
let hasPermissionForFiltered: boolean[] = [];
if (input?.withPermission) {
const permissionService = new PermissionCheckService();
const { permission, fallbackRoles } = input.withPermission;
const permissionChecks = await Promise.all(
teamsData.map((membership) =>
permissionService.checkPermission({
userId: ctx.user.id,
teamId: membership.team.id,
permission: permission as PermissionString,
fallbackRoles: fallbackRoles ? (fallbackRoles as MembershipRole[]) : [],
})
)
);
// Store permission results for teams that passed the filter
hasPermissionForFiltered = permissionChecks.filter((hasPermission) => hasPermission);
teamsData = teamsData.filter((_, index) => permissionChecks[index]);
}
return [
{
teamId: null,
@@ -99,7 +125,7 @@ export const teamsAndUserProfilesQuery = async ({ ctx, input }: TeamsAndUserProf
}),
readOnly: false,
},
...teamsData.map((membership) => ({
...teamsData.map((membership, index) => ({
teamId: membership.team.id,
name: membership.team.name,
slug: membership.team.slug ? `team/${membership.team.slug}` : null,
@@ -107,7 +133,9 @@ export const teamsAndUserProfilesQuery = async ({ ctx, input }: TeamsAndUserProf
? getPlaceholderAvatar(membership.team.parent.logoUrl, membership.team.parent.name)
: getPlaceholderAvatar(membership.team.logoUrl, membership.team.name),
role: membership.role,
readOnly: !withRoleCanCreateEntity(membership.role),
readOnly: input?.withPermission
? !hasPermissionForFiltered[index]
: !withRoleCanCreateEntity(membership.role),
})),
];
};