Skip to content

Commit 1182460

Browse files
chore: add permission checks to invite member and list team members handlers (calcom#25679)
* chore: update invite team member handler * chore: update invite team member handler * fix: add mock for PermissionCheckService in inviteMember handler tests Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: add mocks for PBAC dependencies in inviteMember handler tests Co-Authored-By: morgan@cal.com <morgan@cal.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent aedc0f5 commit 1182460

3 files changed

Lines changed: 69 additions & 2 deletions

File tree

packages/trpc/server/routers/viewer/organizations/listOtherTeamMembers.handler.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import z from "zod";
22

33
import { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync";
4+
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
45
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
56
import { prisma } from "@calcom/prisma";
6-
import type { Prisma } from "@calcom/prisma/client";
7+
import { MembershipRole, type Prisma } from "@calcom/prisma/client";
78

89
import { TRPCError } from "@trpc/server";
910

@@ -26,7 +27,7 @@ type ListOptions = {
2627
input: TListOtherTeamMembersSchema;
2728
};
2829

29-
export const listOtherTeamMembers = async ({ input }: ListOptions) => {
30+
export const listOtherTeamMembers = async ({ ctx, input }: ListOptions) => {
3031
const whereConditional: Prisma.MembershipWhereInput = {
3132
teamId: input.teamId,
3233
};
@@ -76,6 +77,28 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
7677
});
7778
}
7879

80+
if (!team.parentId) {
81+
throw new TRPCError({
82+
code: "BAD_REQUEST",
83+
message: "Team does not belong to an organization",
84+
});
85+
}
86+
87+
const permissionCheckService = new PermissionCheckService();
88+
const hasPermission = await permissionCheckService.checkPermission({
89+
userId: ctx.user.id,
90+
teamId: team.parentId,
91+
permission: "team.listMembers",
92+
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
93+
});
94+
95+
if (!hasPermission) {
96+
throw new TRPCError({
97+
code: "FORBIDDEN",
98+
message: "You are not authorized to view team members in this organization",
99+
});
100+
}
101+
79102
const members = await prisma.membership.findMany({
80103
where: whereConditional,
81104
select: {

packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,34 @@ vi.mock("@calcom/prisma", () => {
4141
};
4242
});
4343

44+
// Mock PBAC dependencies - these need to be mocked before PermissionCheckService
45+
vi.mock("@calcom/features/pbac/infrastructure/repositories/PermissionRepository");
46+
vi.mock("@calcom/features/flags/features.repository");
47+
vi.mock("@calcom/features/membership/repositories/MembershipRepository");
48+
vi.mock("@calcom/features/pbac/services/permission.service", () => {
49+
return {
50+
PermissionService: vi.fn().mockImplementation(() => ({
51+
validatePermission: vi.fn().mockReturnValue({ isValid: true }),
52+
validatePermissions: vi.fn().mockReturnValue({ isValid: true }),
53+
})),
54+
};
55+
});
56+
57+
vi.mock("@calcom/features/pbac/services/permission-check.service", () => {
58+
return {
59+
PermissionCheckService: vi.fn().mockImplementation(() => ({
60+
checkPermission: vi.fn().mockResolvedValue(true),
61+
checkPermissions: vi.fn().mockResolvedValue(true),
62+
getUserPermissions: vi.fn().mockResolvedValue([]),
63+
getResourcePermissions: vi.fn().mockResolvedValue([]),
64+
getTeamIdsWithPermission: vi.fn().mockResolvedValue([]),
65+
getTeamIdsWithPermissions: vi.fn().mockResolvedValue([]),
66+
})),
67+
};
68+
});
69+
4470
function fakeNoUsersFoundMatchingInvitations(args: {
71+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4572
team: any;
4673
invitations: {
4774
role: MembershipRole;

packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type TFunction } from "i18next";
22

33
import { getTeamBillingServiceFactory } from "@calcom/ee/billing/di/containers/Billing";
4+
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
45
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
56
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
67
import logger from "@calcom/lib/logger";
@@ -247,6 +248,22 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => {
247248
const { usernameOrEmail, role, isPlatform, creationSource } = input;
248249

249250
const team = await getTeamOrThrow(input.teamId);
251+
252+
const permissionCheckService = new PermissionCheckService();
253+
const hasPermission = await permissionCheckService.checkPermission({
254+
userId: ctx.user.id,
255+
teamId: team.id,
256+
permission: "team.invite",
257+
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
258+
});
259+
260+
if (!hasPermission) {
261+
throw new TRPCError({
262+
code: "FORBIDDEN",
263+
message: "You are not authorized to invite team members in this organization's team",
264+
});
265+
}
266+
250267
const requestedSlugForTeam = team?.metadata?.requestedSlug ?? null;
251268
const isTeamAnOrg = team.isOrganization;
252269
const organization = inviter.profile.organization;

0 commit comments

Comments
 (0)