chore: add permission checks to invite member and list team members handlers (#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>
This commit is contained in:
Morgan
2025-12-07 16:32:55 +00:00
committed by GitHub
co-authored by morgan@cal.com Devin AI
parent aedc0f5a03
commit 1182460d5c
3 changed files with 69 additions and 2 deletions
@@ -1,9 +1,10 @@
import z from "zod";
import { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole, type Prisma } from "@calcom/prisma/client";
import { TRPCError } from "@trpc/server";
@@ -26,7 +27,7 @@ type ListOptions = {
input: TListOtherTeamMembersSchema;
};
export const listOtherTeamMembers = async ({ input }: ListOptions) => {
export const listOtherTeamMembers = async ({ ctx, input }: ListOptions) => {
const whereConditional: Prisma.MembershipWhereInput = {
teamId: input.teamId,
};
@@ -76,6 +77,28 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
});
}
if (!team.parentId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Team does not belong to an organization",
});
}
const permissionCheckService = new PermissionCheckService();
const hasPermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: team.parentId,
permission: "team.listMembers",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
if (!hasPermission) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not authorized to view team members in this organization",
});
}
const members = await prisma.membership.findMany({
where: whereConditional,
select: {
@@ -41,7 +41,34 @@ vi.mock("@calcom/prisma", () => {
};
});
// Mock PBAC dependencies - these need to be mocked before PermissionCheckService
vi.mock("@calcom/features/pbac/infrastructure/repositories/PermissionRepository");
vi.mock("@calcom/features/flags/features.repository");
vi.mock("@calcom/features/membership/repositories/MembershipRepository");
vi.mock("@calcom/features/pbac/services/permission.service", () => {
return {
PermissionService: vi.fn().mockImplementation(() => ({
validatePermission: vi.fn().mockReturnValue({ isValid: true }),
validatePermissions: vi.fn().mockReturnValue({ isValid: true }),
})),
};
});
vi.mock("@calcom/features/pbac/services/permission-check.service", () => {
return {
PermissionCheckService: vi.fn().mockImplementation(() => ({
checkPermission: vi.fn().mockResolvedValue(true),
checkPermissions: vi.fn().mockResolvedValue(true),
getUserPermissions: vi.fn().mockResolvedValue([]),
getResourcePermissions: vi.fn().mockResolvedValue([]),
getTeamIdsWithPermission: vi.fn().mockResolvedValue([]),
getTeamIdsWithPermissions: vi.fn().mockResolvedValue([]),
})),
};
});
function fakeNoUsersFoundMatchingInvitations(args: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
team: any;
invitations: {
role: MembershipRole;
@@ -1,6 +1,7 @@
import { type TFunction } from "i18next";
import { getTeamBillingServiceFactory } from "@calcom/ee/billing/di/containers/Billing";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import logger from "@calcom/lib/logger";
@@ -247,6 +248,22 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => {
const { usernameOrEmail, role, isPlatform, creationSource } = input;
const team = await getTeamOrThrow(input.teamId);
const permissionCheckService = new PermissionCheckService();
const hasPermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: team.id,
permission: "team.invite",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
if (!hasPermission) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not authorized to invite team members in this organization's team",
});
}
const requestedSlugForTeam = team?.metadata?.requestedSlug ?? null;
const isTeamAnOrg = team.isOrganization;
const organization = inviter.profile.organization;