From 8d7cdf9d3ba94ead52ad618621bb85e7a4b2b0fe Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Thu, 25 Sep 2025 03:54:16 -0400 Subject: [PATCH] refactor: use permission check service for isTeamAdmin (#24026) * saml * addGuests.handler.ts * rename canAccess to canAccessOrganization and use organization.read * update usages * team update handler * team member invite * event type update * remove isTeamAdmin util * updateInternalNotesPresets * setInviteExpiration * event type update * deleteInvite * createInvite * team publish * fixes * final * address feedback * update test --- .../server/userCanCreateTeamGroupMapping.ts | 4 +- packages/features/ee/sso/lib/saml.ts | 22 ++++- packages/features/ee/teams/lib/queries.ts | 23 ----- .../pbac/services/role-management.factory.ts | 22 +++-- .../viewer/bookings/addGuests.handler.ts | 21 +++-- .../routers/viewer/dsync/create.handler.ts | 4 +- .../routers/viewer/dsync/delete.handler.ts | 4 +- .../routers/viewer/dsync/get.handler.ts | 4 +- .../routers/viewer/sso/delete.handler.ts | 9 +- .../server/routers/viewer/sso/get.handler.ts | 4 +- .../routers/viewer/sso/update.handler.ts | 9 +- .../routers/viewer/sso/updateOIDC.handler.ts | 9 +- .../viewer/teams/createInvite.handler.ts | 25 +++++- .../viewer/teams/deleteInvite.handler.ts | 21 ++++- .../inviteMember/inviteMemberUtils.test.ts | 90 ++++++++----------- .../viewer/teams/inviteMember/utils.ts | 23 +++-- .../routers/viewer/teams/publish.handler.ts | 24 +++-- .../removeHostsFromEventTypes.handler.ts | 17 ++-- .../teams/setInviteExpiration.handler.ts | 21 ++++- .../routers/viewer/teams/update.handler.ts | 14 ++- .../updateInternalNotesPresets.handler.ts | 13 ++- 21 files changed, 238 insertions(+), 145 deletions(-) diff --git a/packages/features/ee/dsync/lib/server/userCanCreateTeamGroupMapping.ts b/packages/features/ee/dsync/lib/server/userCanCreateTeamGroupMapping.ts index 07d99caf7d..41f5d844e4 100644 --- a/packages/features/ee/dsync/lib/server/userCanCreateTeamGroupMapping.ts +++ b/packages/features/ee/dsync/lib/server/userCanCreateTeamGroupMapping.ts @@ -1,4 +1,4 @@ -import { canAccess } from "@calcom/features/ee/sso/lib/saml"; +import { canAccessOrganization } from "@calcom/features/ee/sso/lib/saml"; import prisma from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; @@ -16,7 +16,7 @@ const userCanCreateTeamGroupMapping = async ( }); } - const { message, access } = await canAccess(user, organizationId); + const { message, access } = await canAccessOrganization(user, organizationId); if (!access) { throw new TRPCError({ code: "BAD_REQUEST", diff --git a/packages/features/ee/sso/lib/saml.ts b/packages/features/ee/sso/lib/saml.ts index a409338e7f..f485f2be06 100644 --- a/packages/features/ee/sso/lib/saml.ts +++ b/packages/features/ee/sso/lib/saml.ts @@ -1,7 +1,8 @@ import type { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { MembershipRole } from "@calcom/prisma/enums"; export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || ""; export const isSAMLLoginEnabled = samlDatabaseUrl.length > 0; @@ -28,7 +29,7 @@ export const isSAMLAdmin = (email: string) => { return false; }; -export const canAccess = async (user: { id: number; email: string }, teamId: number | null) => { +export const canAccessOrganization = async (user: { id: number; email: string }, teamId: number | null) => { const { id: userId, email } = user; if (!isSAMLLoginEnabled) { @@ -40,7 +41,22 @@ export const canAccess = async (user: { id: number; email: string }, teamId: num // Hosted if (HOSTED_CAL_FEATURES) { - if (teamId === null || !(await isTeamAdmin(userId, teamId))) { + if (teamId === null) { + return { + message: "dont_have_permission", + access: false, + }; + } + + const permissionCheckService = new PermissionCheckService(); + const hasPermission = await permissionCheckService.checkPermission({ + userId, + teamId, + permission: "organization.read", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasPermission) { return { message: "dont_have_permission", access: false, diff --git a/packages/features/ee/teams/lib/queries.ts b/packages/features/ee/teams/lib/queries.ts index 0ccb517c95..a90f4a1d50 100644 --- a/packages/features/ee/teams/lib/queries.ts +++ b/packages/features/ee/teams/lib/queries.ts @@ -385,29 +385,6 @@ export async function getTeamWithoutMembers(args: { }; } -// also returns team -export async function isTeamAdmin(userId: number, teamId: number) { - const team = await prisma.membership.findFirst({ - where: { - userId, - teamId, - accepted: true, - OR: [{ role: "ADMIN" }, { role: "OWNER" }], - }, - include: { - team: { - select: { - metadata: true, - parentId: true, - isOrganization: true, - }, - }, - }, - }); - if (!team) return false; - return team; -} - export async function isTeamOwner(userId: number, teamId: number) { return !!(await prisma.membership.findFirst({ where: { diff --git a/packages/features/pbac/services/role-management.factory.ts b/packages/features/pbac/services/role-management.factory.ts index fb5928b3c3..2f90860e47 100644 --- a/packages/features/pbac/services/role-management.factory.ts +++ b/packages/features/pbac/services/role-management.factory.ts @@ -1,6 +1,5 @@ import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -97,12 +96,23 @@ class PBACRoleManager implements IRoleManager { class LegacyRoleManager implements IRoleManager { public isPBACEnabled = false; async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise { - const membership = - scope === "team" - ? !!(await isTeamAdmin(userId, targetId)) - : !!(await isOrganisationAdmin(userId, targetId)); + let hasPermission = false; + if (scope === "team") { + const team = await prisma.membership.findFirst({ + where: { + userId, + teamId: targetId, + accepted: true, + OR: [{ role: "ADMIN" }, { role: "OWNER" }], + }, + }); + hasPermission = !!team; + } else { + hasPermission = !!(await isOrganisationAdmin(userId, targetId)); + } + // Only OWNER/ADMIN can update role - if (!membership) { + if (!hasPermission) { throw new RoleManagementError( "Only owners or admin can update roles", RoleManagementErrorCode.UNAUTHORIZED diff --git a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts index 3262b70d4c..5d872cd04d 100644 --- a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts @@ -1,11 +1,12 @@ import dayjs from "@calcom/dayjs"; import { sendAddGuestsEmails } from "@calcom/emails"; import EventManager from "@calcom/features/bookings/lib/EventManager"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { isTeamAdmin, isTeamOwner } from "@calcom/features/ee/teams/lib/queries"; import { prisma } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { BookingResponses } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -44,15 +45,21 @@ export const addGuestsHandler = async ({ ctx, input }: AddGuestsOptions) => { if (!booking) throw new TRPCError({ code: "NOT_FOUND", message: "booking_not_found" }); - const isTeamAdminOrOwner = - (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) || - (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0)); - const isOrganizer = booking.userId === user.id; - const isAttendee = !!booking.attendees.find((attendee) => attendee.email === user.email); - if (!isTeamAdminOrOwner && !isOrganizer && !isAttendee) { + let hasBookingUpdatePermission = false; + if (booking.eventType?.teamId) { + const permissionCheckService = new PermissionCheckService(); + hasBookingUpdatePermission = await permissionCheckService.checkPermission({ + userId: user.id, + teamId: booking.eventType.teamId, + permission: "booking.update", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + } + + if (!hasBookingUpdatePermission && !isOrganizer && !isAttendee) { throw new TRPCError({ code: "FORBIDDEN", message: "you_do_not_have_permission" }); } diff --git a/packages/trpc/server/routers/viewer/dsync/create.handler.ts b/packages/trpc/server/routers/viewer/dsync/create.handler.ts index 1c456281ae..a35f78c10c 100644 --- a/packages/trpc/server/routers/viewer/dsync/create.handler.ts +++ b/packages/trpc/server/routers/viewer/dsync/create.handler.ts @@ -1,7 +1,7 @@ import type { DirectoryType } from "@boxyhq/saml-jackson"; import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { canAccess, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; +import { canAccessOrganization, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; import prisma from "@calcom/prisma"; import { TRPCError } from "@trpc/server"; @@ -21,7 +21,7 @@ export const createHandler = async ({ ctx, input }: Options) => { const { organizationId } = input; const { dsyncController } = await jackson(); - const { message, access } = await canAccess(ctx.user, organizationId); + const { message, access } = await canAccessOrganization(ctx.user, organizationId); if (!access) { throw new TRPCError({ diff --git a/packages/trpc/server/routers/viewer/dsync/delete.handler.ts b/packages/trpc/server/routers/viewer/dsync/delete.handler.ts index b4c73d4931..488247a46f 100644 --- a/packages/trpc/server/routers/viewer/dsync/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/dsync/delete.handler.ts @@ -1,5 +1,5 @@ import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { canAccess } from "@calcom/features/ee/sso/lib/saml"; +import { canAccessOrganization } from "@calcom/features/ee/sso/lib/saml"; import prisma from "@calcom/prisma"; import { TRPCError } from "@trpc/server"; @@ -18,7 +18,7 @@ type Options = { export const deleteHandler = async ({ ctx, input }: Options) => { const { dsyncController } = await jackson(); - const { message, access } = await canAccess(ctx.user, input.organizationId); + const { message, access } = await canAccessOrganization(ctx.user, input.organizationId); if (!access) { throw new TRPCError({ diff --git a/packages/trpc/server/routers/viewer/dsync/get.handler.ts b/packages/trpc/server/routers/viewer/dsync/get.handler.ts index 2811dd9525..e628f4e021 100644 --- a/packages/trpc/server/routers/viewer/dsync/get.handler.ts +++ b/packages/trpc/server/routers/viewer/dsync/get.handler.ts @@ -1,5 +1,5 @@ import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { canAccess } from "@calcom/features/ee/sso/lib/saml"; +import { canAccessOrganization } from "@calcom/features/ee/sso/lib/saml"; import prisma from "@calcom/prisma"; import { TRPCError } from "@trpc/server"; @@ -18,7 +18,7 @@ type Options = { export const getHandler = async ({ ctx, input }: Options) => { const { dsyncController } = await jackson(); - const { message, access } = await canAccess(ctx.user, input.organizationId); + const { message, access } = await canAccessOrganization(ctx.user, input.organizationId); if (!access) { throw new TRPCError({ diff --git a/packages/trpc/server/routers/viewer/sso/delete.handler.ts b/packages/trpc/server/routers/viewer/sso/delete.handler.ts index a9e1714e64..a09aeb2579 100644 --- a/packages/trpc/server/routers/viewer/sso/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/sso/delete.handler.ts @@ -1,5 +1,10 @@ import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { canAccess, samlProductID, samlTenantID, tenantPrefix } from "@calcom/features/ee/sso/lib/saml"; +import { + canAccessOrganization, + samlProductID, + samlTenantID, + tenantPrefix, +} from "@calcom/features/ee/sso/lib/saml"; import { TRPCError } from "@trpc/server"; @@ -18,7 +23,7 @@ export const deleteHandler = async ({ ctx, input }: DeleteOptions) => { const { teamId } = input; - const { message, access } = await canAccess(ctx.user, teamId); + const { message, access } = await canAccessOrganization(ctx.user, teamId); if (!access) { throw new TRPCError({ diff --git a/packages/trpc/server/routers/viewer/sso/get.handler.ts b/packages/trpc/server/routers/viewer/sso/get.handler.ts index b875de63da..c5b51fbeda 100644 --- a/packages/trpc/server/routers/viewer/sso/get.handler.ts +++ b/packages/trpc/server/routers/viewer/sso/get.handler.ts @@ -1,7 +1,7 @@ import jackson from "@calcom/features/ee/sso/lib/jackson"; import type { SSOConnection } from "@calcom/features/ee/sso/lib/saml"; import { - canAccess, + canAccessOrganization, oidcPath, samlProductID, samlTenantID, @@ -23,7 +23,7 @@ type GetOptions = { export const getHandler = async ({ ctx, input }: GetOptions): Promise => { const { teamId } = input; - const { message, access } = await canAccess(ctx.user, teamId); + const { message, access } = await canAccessOrganization(ctx.user, teamId); if (!access) { throw new TRPCError({ diff --git a/packages/trpc/server/routers/viewer/sso/update.handler.ts b/packages/trpc/server/routers/viewer/sso/update.handler.ts index c707aa406b..b1cf80c0b7 100644 --- a/packages/trpc/server/routers/viewer/sso/update.handler.ts +++ b/packages/trpc/server/routers/viewer/sso/update.handler.ts @@ -1,5 +1,10 @@ import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { canAccess, samlProductID, samlTenantID, tenantPrefix } from "@calcom/features/ee/sso/lib/saml"; +import { + canAccessOrganization, + samlProductID, + samlTenantID, + tenantPrefix, +} from "@calcom/features/ee/sso/lib/saml"; import { TRPCError } from "@trpc/server"; @@ -18,7 +23,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { const { encodedRawMetadata, teamId } = input; - const { message, access } = await canAccess(ctx.user, teamId); + const { message, access } = await canAccessOrganization(ctx.user, teamId); if (!access) { throw new TRPCError({ diff --git a/packages/trpc/server/routers/viewer/sso/updateOIDC.handler.ts b/packages/trpc/server/routers/viewer/sso/updateOIDC.handler.ts index 483bbf5b64..db25767e67 100644 --- a/packages/trpc/server/routers/viewer/sso/updateOIDC.handler.ts +++ b/packages/trpc/server/routers/viewer/sso/updateOIDC.handler.ts @@ -1,5 +1,10 @@ import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { canAccess, samlProductID, samlTenantID, tenantPrefix } from "@calcom/features/ee/sso/lib/saml"; +import { + canAccessOrganization, + samlProductID, + samlTenantID, + tenantPrefix, +} from "@calcom/features/ee/sso/lib/saml"; import { TRPCError } from "@trpc/server"; @@ -16,7 +21,7 @@ type UpdateOIDCOptions = { export const updateOIDCHandler = async ({ ctx, input }: UpdateOIDCOptions) => { const { teamId, clientId, clientSecret, wellKnownUrl } = input; - const { message, access } = await canAccess(ctx.user, teamId); + const { message, access } = await canAccessOrganization(ctx.user, teamId); if (!access) { throw new TRPCError({ diff --git a/packages/trpc/server/routers/viewer/teams/createInvite.handler.ts b/packages/trpc/server/routers/viewer/teams/createInvite.handler.ts index d066f867af..3b9b9db247 100644 --- a/packages/trpc/server/routers/viewer/teams/createInvite.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/createInvite.handler.ts @@ -1,8 +1,9 @@ import { randomBytes } from "crypto"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { WEBAPP_URL } from "@calcom/lib/constants"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; import { prisma } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -18,10 +19,26 @@ type CreateInviteOptions = { export const createInviteHandler = async ({ ctx, input }: CreateInviteOptions) => { const { teamId } = input; - const membership = await isTeamAdmin(ctx.user.id, teamId); - if (!membership || !membership?.team) throw new TRPCError({ code: "UNAUTHORIZED" }); - const isOrganizationOrATeamInOrganization = !!(membership.team?.parentId || membership.team.isOrganization); + const team = await prisma.team.findUnique({ + where: { id: teamId }, + select: { parentId: true, isOrganization: true }, + }); + + if (!team) throw new TRPCError({ code: "NOT_FOUND" }); + + const permissionCheckService = new PermissionCheckService(); + const permission = team.isOrganization ? "organization.invite" : "team.invite"; + const hasInvitePermission = await permissionCheckService.checkPermission({ + userId: ctx.user.id, + teamId, + permission, + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasInvitePermission) throw new TRPCError({ code: "UNAUTHORIZED" }); + + const isOrganizationOrATeamInOrganization = !!(team.parentId || team.isOrganization); if (input.token) { const existingToken = await prisma.verificationToken.findFirst({ diff --git a/packages/trpc/server/routers/viewer/teams/deleteInvite.handler.ts b/packages/trpc/server/routers/viewer/teams/deleteInvite.handler.ts index 670ed1ddd6..8c099890c6 100644 --- a/packages/trpc/server/routers/viewer/teams/deleteInvite.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/deleteInvite.handler.ts @@ -1,5 +1,6 @@ -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { prisma } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -23,12 +24,26 @@ export const deleteInviteHandler = async ({ ctx, input }: DeleteInviteOptions) = select: { teamId: true, id: true, + team: { + select: { isOrganization: true }, + }, }, }); if (!verificationToken) throw new TRPCError({ code: "NOT_FOUND" }); - if (!verificationToken.teamId || !(await isTeamAdmin(ctx.user.id, verificationToken.teamId))) - throw new TRPCError({ code: "UNAUTHORIZED" }); + if (!verificationToken.teamId) throw new TRPCError({ code: "UNAUTHORIZED" }); + + const permissionCheckService = new PermissionCheckService(); + const isOrgContext = !!verificationToken.team?.isOrganization; + const permission = isOrgContext ? "organization.invite" : "team.invite"; + const hasInvitePermission = await permissionCheckService.checkPermission({ + userId: ctx.user.id, + teamId: verificationToken.teamId, + permission, + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasInvitePermission) throw new TRPCError({ code: "UNAUTHORIZED" }); await prisma.verificationToken.delete({ where: { id: verificationToken.id } }); }; diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts index 386325a91d..04a71d3380 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts @@ -1,7 +1,6 @@ import { describe, it, vi, expect } from "vitest"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; -import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { MembershipRole } from "@calcom/prisma/enums"; import { TRPCError } from "@trpc/server"; @@ -25,12 +24,6 @@ vi.mock("@calcom/prisma", () => { }; }); -vi.mock("@calcom/features/ee/teams/lib/queries", () => { - return { - isTeamAdmin: vi.fn(), - }; -}); - vi.mock("@calcom/lib/server/queries/organisations", () => { return { isOrganisationAdmin: vi.fn(), @@ -38,42 +31,11 @@ vi.mock("@calcom/lib/server/queries/organisations", () => { }; }); -const mockedReturnSuccessCheckPerms = { - accepted: true, - disableImpersonation: false, - id: 1, - role: MembershipRole.ADMIN, - userId: 1, - teamId: 1, - createdAt: null, - updatedAt: null, - customRoleId: null, - team: { - id: 1, - name: "Team A", - slug: null, - logo: null, - appLogo: null, - appIconLogo: null, - bio: null, - hideBranding: false, - hideBookATeamMember: false, - createdAt: new Date(), - brandColor: "#292929", - darkBrandColor: "#fafafa", - timeZone: "Europe/London", - weekStart: "Sunday", - theme: null, - timeFormat: null, - metadata: null, - parentId: null, - parent: null, - isPrivate: false, - logoUrl: "", - calVideoLogo: "", - isOrganization: false, - }, -}; +vi.mock("@calcom/features/pbac/services/permission-check.service", () => { + return { + PermissionCheckService: vi.fn(), + }; +}); const mockedRegularTeam: TeamWithParent = { id: 1, @@ -142,27 +104,45 @@ const userInTeamNotAccepted: UserWithMembership = { describe("Invite Member Utils", () => { describe("ensureAtleastAdminPermissions", () => { it("It should throw an error if the user is not an admin of the ORG", async () => { - vi.mocked(isOrganisationAdmin).mockResolvedValue(false); + const mockCheckPermission = vi.fn().mockResolvedValue(false); + vi.mocked(PermissionCheckService).mockImplementation( + () => + ({ + checkPermission: mockCheckPermission, + } as any) + ); + await expect(ensureAtleastAdminPermissions({ userId: 1, teamId: 1, isOrg: true })).rejects.toThrow( "UNAUTHORIZED" ); + + expect(mockCheckPermission).toHaveBeenCalledWith({ + userId: 1, + teamId: 1, + permission: "organization.invite", + fallbackRoles: ["OWNER", "ADMIN"], + }); }); it("It should NOT throw an error if the user is an admin of the ORG", async () => { - vi.mocked(isOrganisationAdmin).mockResolvedValue(mockedReturnSuccessCheckPerms); + const mockCheckPermission = vi.fn().mockResolvedValue(true); + vi.mocked(PermissionCheckService).mockImplementation( + () => + ({ + checkPermission: mockCheckPermission, + } as any) + ); + await expect( ensureAtleastAdminPermissions({ userId: 1, teamId: 1, isOrg: true }) ).resolves.not.toThrow(); - }); - it("It should throw an error if the user is not an admin of the team", async () => { - vi.mocked(isTeamAdmin).mockResolvedValue(false); - await expect(ensureAtleastAdminPermissions({ userId: 1, teamId: 1 })).rejects.toThrow("UNAUTHORIZED"); - }); - - it("It should NOT throw an error if the user is an admin of a team", async () => { - vi.mocked(isTeamAdmin).mockResolvedValue(mockedReturnSuccessCheckPerms); - await expect(ensureAtleastAdminPermissions({ userId: 1, teamId: 1 })).resolves.not.toThrow(); + expect(mockCheckPermission).toHaveBeenCalledWith({ + userId: 1, + teamId: 1, + permission: "organization.invite", + fallbackRoles: ["OWNER", "ADMIN"], + }); }); }); diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index b27dbe9496..0f08735544 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -4,15 +4,14 @@ import type { TFunction } from "i18next"; import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; import { sendTeamInviteEmail } from "@calcom/emails"; import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; +import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { ENABLE_PROFILE_SWITCHER, WEBAPP_URL } from "@calcom/lib/constants"; import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAnExistingUser"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; -import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; import { getParsedTeam } from "@calcom/lib/server/repository/teamUtils"; import { UserRepository } from "@calcom/lib/server/repository/user"; @@ -66,12 +65,20 @@ export async function ensureAtleastAdminPermissions({ teamId: number; isOrg?: boolean; }) { + const permissionCheckService = new PermissionCheckService(); + // Checks if the team they are inviting to IS the org. Not a child team - if (isOrg) { - if (!(await isOrganisationAdmin(userId, teamId))) throw new TRPCError({ code: "UNAUTHORIZED" }); - } else { - // TODO: do some logic here to check if the user is inviting a NEW user to a team that ISNT in the same org - if (!(await isTeamAdmin(userId, teamId))) throw new TRPCError({ code: "UNAUTHORIZED" }); + // TODO: do some logic here to check if the user is inviting a NEW user to a team that ISNT in the same org + const permission = isOrg ? "organization.invite" : "team.invite"; + const hasInvitePermission = await permissionCheckService.checkPermission({ + userId, + teamId, + permission, + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasInvitePermission) { + throw new TRPCError({ code: "UNAUTHORIZED" }); } } diff --git a/packages/trpc/server/routers/viewer/teams/publish.handler.ts b/packages/trpc/server/routers/viewer/teams/publish.handler.ts index 4706fe0ebb..5b353e6902 100644 --- a/packages/trpc/server/routers/viewer/teams/publish.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/publish.handler.ts @@ -1,16 +1,16 @@ import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { Redirect } from "@calcom/lib/redirect"; -import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; import { TeamService } from "@calcom/lib/server/service/teamService"; +import type { Prisma } from "@calcom/prisma/client"; +import { MembershipRole } from "@calcom/prisma/enums"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@trpc/server"; import type { TrpcSessionUser } from "../../../types"; import type { TPublishInputSchema } from "./publish.schema"; -import { Prisma } from "@calcom/prisma/client"; type PublishOptions = { ctx: { @@ -54,10 +54,20 @@ const generateCheckoutSession = async ({ async function checkPermissions({ ctx, input }: PublishOptions) { const { profile } = ctx.user; - if (profile?.organizationId && !isOrganisationAdmin(ctx.user.id, profile.organizationId)) - throw new TRPCError({ code: "UNAUTHORIZED" }); - if (!profile?.organizationId && !(await isTeamAdmin(ctx.user.id, input.teamId))) - throw new TRPCError({ code: "UNAUTHORIZED" }); + const permissionCheckService = new PermissionCheckService(); + + const isOrg = !!profile?.organizationId; + const permission = isOrg ? "organization.update" : "team.update"; + const teamId = isOrg ? profile.organizationId : input.teamId; + + const hasUpdatePermission = await permissionCheckService.checkPermission({ + userId: ctx.user.id, + teamId, + permission, + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasUpdatePermission) throw new TRPCError({ code: "UNAUTHORIZED" }); } export const publishHandler = async ({ ctx, input }: PublishOptions) => { diff --git a/packages/trpc/server/routers/viewer/teams/removeHostsFromEventTypes.handler.ts b/packages/trpc/server/routers/viewer/teams/removeHostsFromEventTypes.handler.ts index d0b3efb4af..e819766fee 100644 --- a/packages/trpc/server/routers/viewer/teams/removeHostsFromEventTypes.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/removeHostsFromEventTypes.handler.ts @@ -1,5 +1,6 @@ -import { isTeamAdmin, isTeamOwner } from "@calcom/features/ee/teams/lib/queries"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import prisma from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import { TRPCError } from "@trpc/server"; @@ -15,11 +16,17 @@ type RemoveHostsFromEventTypes = { export async function removeHostsFromEventTypesHandler({ ctx, input }: RemoveHostsFromEventTypes) { const { userIds, eventTypeIds, teamId } = input; - const isTeamAdminOrOwner = - (await isTeamAdmin(ctx.user.id, teamId)) || (await isTeamOwner(ctx.user.id, teamId)); - // check if user is admin or owner of team - if (!isTeamAdminOrOwner) throw new TRPCError({ code: "UNAUTHORIZED" }); + const permissionCheckService = new PermissionCheckService(); + const hasEventTypeUpdatePermission = await permissionCheckService.checkPermission({ + userId: ctx.user.id, + teamId, + permission: "eventType.update", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + // check if user has permission to update event types + if (!hasEventTypeUpdatePermission) throw new TRPCError({ code: "UNAUTHORIZED" }); return await prisma.host.deleteMany({ where: { diff --git a/packages/trpc/server/routers/viewer/teams/setInviteExpiration.handler.ts b/packages/trpc/server/routers/viewer/teams/setInviteExpiration.handler.ts index 810d85e904..6233b7e54d 100644 --- a/packages/trpc/server/routers/viewer/teams/setInviteExpiration.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/setInviteExpiration.handler.ts @@ -1,5 +1,6 @@ -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { prisma } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -22,12 +23,26 @@ export const setInviteExpirationHandler = async ({ ctx, input }: SetInviteExpira }, select: { teamId: true, + team: { + select: { isOrganization: true }, + }, }, }); if (!verificationToken) throw new TRPCError({ code: "NOT_FOUND" }); - if (!verificationToken.teamId || !(await isTeamAdmin(ctx.user.id, verificationToken.teamId))) - throw new TRPCError({ code: "UNAUTHORIZED" }); + if (!verificationToken.teamId) throw new TRPCError({ code: "UNAUTHORIZED" }); + + const permissionCheckService = new PermissionCheckService(); + const isOrgContext = !!verificationToken.team?.isOrganization; + const permission = isOrgContext ? "organization.invite" : "team.invite"; + const hasInvitePermission = await permissionCheckService.checkPermission({ + userId: ctx.user.id, + teamId: verificationToken.teamId, + permission, + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasInvitePermission) throw new TRPCError({ code: "UNAUTHORIZED" }); const oneDay = 24 * 60 * 60 * 1000; const expires = expiresInDays diff --git a/packages/trpc/server/routers/viewer/teams/update.handler.ts b/packages/trpc/server/routers/viewer/teams/update.handler.ts index c2cbc642b8..989da1d1dc 100644 --- a/packages/trpc/server/routers/viewer/teams/update.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/update.handler.ts @@ -1,12 +1,12 @@ import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema"; import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder"; import { uploadLogo } from "@calcom/lib/server/avatar"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; -import { RedirectType, RRTimestampBasis } from "@calcom/prisma/enums"; +import { MembershipRole, RedirectType, RRTimestampBasis } from "@calcom/prisma/enums"; import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@trpc/server"; @@ -25,7 +25,15 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { const isOrgAdmin = ctx.user?.organization?.isOrgAdmin; if (!isOrgAdmin) { - if (!(await isTeamAdmin(ctx.user?.id, input.id))) { + const permissionCheckService = new PermissionCheckService(); + const hasTeamUpdatePermission = await permissionCheckService.checkPermission({ + userId: ctx.user?.id || 0, + teamId: input.id, + permission: "team.update", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasTeamUpdatePermission) { throw new TRPCError({ code: "UNAUTHORIZED" }); } } diff --git a/packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts b/packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts index 48216327c3..b273632627 100644 --- a/packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts @@ -1,5 +1,6 @@ -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { prisma } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -20,7 +21,15 @@ export const updateInternalNotesPresetsHandler = async ({ const isOrgAdmin = ctx.user?.organization?.isOrgAdmin; if (!isOrgAdmin) { - if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) { + const permissionCheckService = new PermissionCheckService(); + const hasTeamUpdatePermission = await permissionCheckService.checkPermission({ + userId: ctx.user?.id || 0, + teamId: input.teamId, + permission: "team.update", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasTeamUpdatePermission) { throw new TRPCError({ code: "UNAUTHORIZED" }); } }