diff --git a/apps/web/modules/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx b/apps/web/modules/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx index 9e7ab5a47e..db64a4c6ca 100644 --- a/apps/web/modules/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx +++ b/apps/web/modules/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx @@ -23,7 +23,7 @@ const CreateNewOutOfOfficeEntryButton = ({ const me = useMeQuery(); const { data: orgData } = trpc.viewer.organizations.listCurrent.useQuery(); const isOrgAdminOrOwner = orgData && checkAdminOrOwner(orgData.user.role); - const hasTeamOOOAdminAccess = isOrgAdminOrOwner || me?.data?.isTeamAdminOrOwner; + const hasTeamOOOAdminAccess = isOrgAdminOrOwner || me?.data?.canUpdateTeams; const params = useCompatSearchParams(); const selectedTab = params?.get("type") ?? OutOfOfficeTab.MINE; diff --git a/packages/features/ee/organizations/pages/organization.tsx b/packages/features/ee/organizations/pages/organization.tsx index e5700c8f28..314689bea9 100644 --- a/packages/features/ee/organizations/pages/organization.tsx +++ b/packages/features/ee/organizations/pages/organization.tsx @@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { FeaturesRepository } from "@calcom/features/flags/features.repository"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { MembershipRole } from "@calcom/prisma/enums"; export const getServerSideProps = async ({ req }: GetServerSidePropsContext) => { @@ -24,19 +25,15 @@ export const getServerSideProps = async ({ req }: GetServerSidePropsContext) => } as const; } - // Check if logged in user has OWNER/ADMIN role in organization - const membership = await prisma.membership.findUnique({ - where: { - userId_teamId: { - userId: session?.user.id, - teamId: session?.user.profile.organizationId, - }, - }, - select: { - role: true, - }, + const permissionCheckService = new PermissionCheckService(); + const canManageOrganization = await permissionCheckService.checkPermission({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + permission: "organization.update", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], }); - if (!membership?.role || membership?.role === MembershipRole.MEMBER) { + + if (!canManageOrganization) { return { notFound: true, } as const; diff --git a/packages/features/ee/workflows/repositories/WorkflowRepository.ts b/packages/features/ee/workflows/repositories/WorkflowRepository.ts index dbe97d4c7f..825d67e607 100644 --- a/packages/features/ee/workflows/repositories/WorkflowRepository.ts +++ b/packages/features/ee/workflows/repositories/WorkflowRepository.ts @@ -7,6 +7,7 @@ import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/s import type { WorkflowListType as WorkflowType } from "@calcom/ee/workflows/lib/types"; import type { WorkflowStep } from "@calcom/ee/workflows/lib/types"; import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; @@ -242,6 +243,15 @@ export class WorkflowRepository { const filtered = filters && hasFilter(filters); + const permissionCheckService = new PermissionCheckService(); + const teamIdsWithWorkflowUpdatePermission = userId + ? await permissionCheckService.getTeamIdsWithPermission({ + userId, + permission: "workflow.update", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }) + : []; + const allWorkflows = await prisma.workflow.findMany({ where: { OR: [ @@ -273,9 +283,9 @@ export class WorkflowRepository { if (!filtered) { const workflowsWithReadOnly: WorkflowType[] = allWorkflows.map((workflow) => { - const readOnly = !!workflow.team?.members?.find( - (member) => member.userId === userId && member.role === MembershipRole.MEMBER - ); + const readOnly = workflow.teamId + ? !teamIdsWithWorkflowUpdatePermission.includes(workflow.teamId) + : false; return { readOnly, isOrg: workflow.team?.isOrganization ?? false, ...workflow }; }); @@ -325,9 +335,9 @@ export class WorkflowRepository { }); const workflowsWithReadOnly: WorkflowType[] = filteredWorkflows.map((workflow) => { - const readOnly = !!workflow.team?.members?.find( - (member) => member.userId === userId && member.role === MembershipRole.MEMBER - ); + const readOnly = workflow.teamId + ? !teamIdsWithWorkflowUpdatePermission.includes(workflow.teamId) + : false; return { readOnly, isOrg: workflow.team?.isOrganization ?? false, ...workflow }; }); diff --git a/packages/features/eventtypes/lib/getEventTypesByViewer.ts b/packages/features/eventtypes/lib/getEventTypesByViewer.ts index d387660d8b..db00a0525b 100644 --- a/packages/features/eventtypes/lib/getEventTypesByViewer.ts +++ b/packages/features/eventtypes/lib/getEventTypesByViewer.ts @@ -52,13 +52,19 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo shouldListUserEvents = true; } - // Get teams where user has eventType.read permission for PBAC readonly check const permissionCheckService = new PermissionCheckService(); - const teamsWithEventTypeReadPermission = await permissionCheckService.getTeamIdsWithPermission({ - userId: user.id, - permission: "eventType.read", - fallbackRoles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER], - }); + const [teamsWithEventTypeReadPermission, teamsWithEventTypeUpdatePermission] = await Promise.all([ + permissionCheckService.getTeamIdsWithPermission({ + userId: user.id, + permission: "eventType.read", + fallbackRoles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER], + }), + permissionCheckService.getTeamIdsWithPermission({ + userId: user.id, + permission: "eventType.update", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }), + ]); const eventTypeRepo = new EventTypeRepository(prisma); const [profileMemberships, profileEventTypes] = await Promise.all([ @@ -295,7 +301,7 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo return res; }) .filter((evType) => - membership.role === MembershipRole.MEMBER + !teamsWithEventTypeUpdatePermission.includes(team.id) ? evType.schedulingType !== SchedulingType.MANAGED : true ) diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index 2148f78e1f..2cbb5a8fc5 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -7,7 +7,9 @@ import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/ import { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync"; import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; import { getDefaultEvent, getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; +import { MembershipRole } from "@calcom/prisma/enums"; import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; @@ -517,25 +519,27 @@ export const getPublicEvent = async ( length: eventWithUserProfiles.length, }); } - const isTeamAdminOrOwner = await prisma.membership.findFirst({ - where: { - userId: currentUserId ?? -1, - teamId: event.teamId ?? -1, - accepted: true, - role: { in: ["ADMIN", "OWNER"] }, - }, - }); + let canViewPrivateTeamMembers = false; + if (currentUserId && event.teamId) { + const permissionCheckService = new PermissionCheckService(); + canViewPrivateTeamMembers = await permissionCheckService.checkPermission({ + userId: currentUserId, + teamId: event.teamId, + permission: "team.read", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }); - const isOrgAdminOrOwner = await prisma.membership.findFirst({ - where: { - userId: currentUserId ?? -1, - teamId: event.team?.parentId ?? -1, - accepted: true, - role: { in: ["ADMIN", "OWNER"] }, - }, - }); + if (!canViewPrivateTeamMembers && event.team?.parentId) { + canViewPrivateTeamMembers = await permissionCheckService.checkPermission({ + userId: currentUserId, + teamId: event.team.parentId, + permission: "team.read", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }); + } + } - if (event.team?.isPrivate && !isTeamAdminOrOwner && !isOrgAdminOrOwner) { + if (event.team?.isPrivate && !canViewPrivateTeamMembers) { users = []; } diff --git a/packages/trpc/server/routers/viewer/eventTypes/getActiveOnOptions.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getActiveOnOptions.handler.ts index dc6f22eb39..49b3d449be 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getActiveOnOptions.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getActiveOnOptions.handler.ts @@ -1,5 +1,6 @@ import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; import { PrismaRoutingFormRepository } from "@calcom/features/routing-forms/repositories/PrismaRoutingFormRepository"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; @@ -55,12 +56,14 @@ const fetchEventTypeGroups = async ({ parentOrgHasLockedEventTypes, skipEventTypes, teamId, + teamIdsWithEventTypeUpdatePermission, }: { ctx: { user: NonNullable; prisma: PrismaClient }; profile: NonNullable>>; parentOrgHasLockedEventTypes: boolean | undefined; skipEventTypes: boolean; teamId?: number; + teamIdsWithEventTypeUpdatePermission: number[]; }): Promise => { const user = ctx.user; const userProfile = ctx.user.profile; @@ -132,12 +135,11 @@ const fetchEventTypeGroups = async ({ metadata: teamMetadataSchema.parse(membership.team.metadata), }; + const canUpdateEventTypes = teamIdsWithEventTypeUpdatePermission.includes(team.id); const eventTypes = team.eventTypes ?.filter((evType) => evType.userId === null || evType.userId === user.id) ?.filter((evType) => - membership.role === MembershipRole.MEMBER - ? evType.schedulingType !== SchedulingType.MANAGED - : true + !canUpdateEventTypes ? evType.schedulingType !== SchedulingType.MANAGED : true ); return { @@ -222,12 +224,20 @@ export const getActiveOnOptions = async ({ ctx, input }: GetActiveOnOptions) => throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } + const permissionCheckService = new PermissionCheckService(); + const teamIdsWithEventTypeUpdatePermission = await permissionCheckService.getTeamIdsWithPermission({ + userId: user.id, + permission: "eventType.update", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }); + const eventTypeGroups = await fetchEventTypeGroups({ ctx, profile, parentOrgHasLockedEventTypes, skipEventTypes: shouldSkipEventTypes, teamId, + teamIdsWithEventTypeUpdatePermission, }); const teamOptions = await fetchTeamOptions({ diff --git a/packages/trpc/server/routers/viewer/me/checkForInvalidAppCredentials.ts b/packages/trpc/server/routers/viewer/me/checkForInvalidAppCredentials.ts index 11d805f8c0..516d26af8d 100644 --- a/packages/trpc/server/routers/viewer/me/checkForInvalidAppCredentials.ts +++ b/packages/trpc/server/routers/viewer/me/checkForInvalidAppCredentials.ts @@ -1,4 +1,5 @@ import { getAppFromSlug } from "@calcom/app-store/utils"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { type InvalidAppCredentialBannerProps } from "@calcom/features/users/types/invalidAppCredentials"; import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -13,21 +14,16 @@ type checkInvalidAppCredentialsOptions = { export const checkInvalidAppCredentials = async ({ ctx }: checkInvalidAppCredentialsOptions) => { const userId = ctx.user.id; - // First get the teams where user is admin/owner - const userTeamIds = await prisma.membership.findMany({ - where: { - userId: userId, - accepted: true, - role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] }, - }, - select: { - teamId: true, - }, + const permissionCheckService = new PermissionCheckService(); + const userTeamIds = await permissionCheckService.getTeamIdsWithPermission({ + userId, + permission: "team.update", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], }); const apps = await prisma.credential.findMany({ where: { - OR: [{ userId }, { teamId: { in: userTeamIds.map((membership) => membership.teamId) } }], + OR: [{ userId }, { teamId: { in: userTeamIds } }], invalid: true, }, select: { diff --git a/packages/trpc/server/routers/viewer/me/get.handler.ts b/packages/trpc/server/routers/viewer/me/get.handler.ts index 2da2dadeaf..12cb00c88e 100644 --- a/packages/trpc/server/routers/viewer/me/get.handler.ts +++ b/packages/trpc/server/routers/viewer/me/get.handler.ts @@ -1,5 +1,6 @@ import type { Session } from "next-auth"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; @@ -94,17 +95,13 @@ export const getHandler = async ({ ctx, input }: MeOptions) => { organizationSettings: user?.profile?.organization?.organizationSettings, }; - const isTeamAdminOrOwner = - (await prisma.membership.findFirst({ - where: { - userId: user.id, - accepted: true, - role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] }, - }, - select: { - id: true, - }, - })) !== null; + const permissionCheckService = new PermissionCheckService(); + const teamsWithWritePermission = await permissionCheckService.getTeamIdsWithPermission({ + userId: user.id, + permission: "team.update", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }); + const canUpdateTeams = teamsWithWritePermission.length > 0; return { id: user.id, @@ -145,6 +142,6 @@ export const getHandler = async ({ ctx, input }: MeOptions) => { secondaryEmails, isPremium: userMetadataPrased?.isPremium, ...(passwordAdded ? { passwordAdded } : {}), - isTeamAdminOrOwner, + canUpdateTeams, }; }; diff --git a/packages/trpc/server/routers/viewer/ooo/outOfOffice.utils.ts b/packages/trpc/server/routers/viewer/ooo/outOfOffice.utils.ts index 8642852f3a..382e834787 100644 --- a/packages/trpc/server/routers/viewer/ooo/outOfOffice.utils.ts +++ b/packages/trpc/server/routers/viewer/ooo/outOfOffice.utils.ts @@ -1,21 +1,18 @@ +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import prisma from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; export const isAdminForUser = async (adminUserId: number, memberUserId: number) => { - const adminTeams = await prisma.membership.findMany({ - where: { - userId: adminUserId, - accepted: true, - role: { - in: [MembershipRole.ADMIN, MembershipRole.OWNER], - }, - }, - select: { - teamId: true, - }, + const permissionCheckService = new PermissionCheckService(); + const adminTeamIds = await permissionCheckService.getTeamIdsWithPermission({ + userId: adminUserId, + permission: "ooo.update", + fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], }); - const adminTeamIds = adminTeams?.map((team) => team.teamId); + if (adminTeamIds.length === 0) { + return false; + } const member = await prisma.membership.findFirst({ where: { diff --git a/packages/trpc/server/routers/viewer/organizations/checkIfOrgNeedsUpgrade.handler.ts b/packages/trpc/server/routers/viewer/organizations/checkIfOrgNeedsUpgrade.handler.ts index cb0ff8bebd..28fc9f825a 100644 --- a/packages/trpc/server/routers/viewer/organizations/checkIfOrgNeedsUpgrade.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/checkIfOrgNeedsUpgrade.handler.ts @@ -1,3 +1,4 @@ +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -14,16 +15,22 @@ type GetUpgradeableOptions = { export async function checkIfOrgNeedsUpgradeHandler({ ctx }: GetUpgradeableOptions) { if (!IS_TEAM_BILLING_ENABLED) return []; - // Get all teams/orgs where the user is an owner + const permissionCheckService = new PermissionCheckService(); + const teamIdsWithBillingPermission = await permissionCheckService.getTeamIdsWithPermission({ + userId: ctx.user.id, + permission: "organization.manageBilling", + fallbackRoles: [MembershipRole.OWNER], + }); + + if (teamIdsWithBillingPermission.length === 0) return []; + let teams = await prisma.membership.findMany({ where: { - user: { - id: ctx.user.id, - }, - role: MembershipRole.OWNER, + userId: ctx.user.id, + teamId: { in: teamIdsWithBillingPermission }, team: { isPlatform: false, - parentId: null, // Since ORGS relay on their parent's subscription, we don't need to return them + parentId: null, }, }, include: {