diff --git a/apps/api/v1/lib/helpers/verifyApiKey.ts b/apps/api/v1/lib/helpers/verifyApiKey.ts index 8b8114888e..23cbe74d43 100644 --- a/apps/api/v1/lib/helpers/verifyApiKey.ts +++ b/apps/api/v1/lib/helpers/verifyApiKey.ts @@ -6,6 +6,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants"; import prisma from "@calcom/prisma"; import { isAdminGuard } from "../utils/isAdmin"; +import { ScopeOfAdmin } from "../utils/scopeOfAdmin"; // Used to check if the apiKey is not expired, could be extracted if reused. but not for now. export const dateNotInPast = function (date: Date) { @@ -36,7 +37,10 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => { if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" }); // save the user id in the request for later use req.userId = apiKey.userId; - // save the isAdmin boolean here for later use - req.isAdmin = await isAdminGuard(req); + const { isAdmin, scope } = await isAdminGuard(req); + + req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide; + req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin; + await next(); }; diff --git a/apps/api/v1/lib/helpers/verifyCredentialSyncEnabled.ts b/apps/api/v1/lib/helpers/verifyCredentialSyncEnabled.ts index a797bb2db2..b7922ae54d 100644 --- a/apps/api/v1/lib/helpers/verifyCredentialSyncEnabled.ts +++ b/apps/api/v1/lib/helpers/verifyCredentialSyncEnabled.ts @@ -3,9 +3,9 @@ import type { NextMiddleware } from "next-api-middleware"; import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants"; export const verifyCredentialSyncEnabled: NextMiddleware = async (req, res, next) => { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; - if (!isAdmin) { + if (!isSystemWideAdmin) { return res.status(403).json({ error: "Only admin API keys can access credential syncing endpoints" }); } diff --git a/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts b/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts index 2cb69377c1..8faffc4b98 100644 --- a/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts +++ b/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts @@ -4,9 +4,9 @@ import { HttpError } from "@calcom/lib/http-error"; import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; -export function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) { +export function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) { /** Guard: Only admins can query other users */ - if (!isAdmin) { + if (!isSystemWideAdmin) { throw new HttpError({ statusCode: 401, message: "ADMIN required" }); } const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query); diff --git a/apps/api/v1/lib/utils/isAdmin.ts b/apps/api/v1/lib/utils/isAdmin.ts index 8b90c37867..1caf210a98 100644 --- a/apps/api/v1/lib/utils/isAdmin.ts +++ b/apps/api/v1/lib/utils/isAdmin.ts @@ -1,10 +1,37 @@ import type { NextApiRequest } from "next"; import prisma from "@calcom/prisma"; -import { UserPermissionRole } from "@calcom/prisma/enums"; +import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums"; + +import { ScopeOfAdmin } from "./scopeOfAdmin"; export const isAdminGuard = async (req: NextApiRequest) => { const { userId } = req; - const user = await prisma.user.findUnique({ where: { id: userId } }); - return user?.role === UserPermissionRole.ADMIN; + const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } }); + if (!user) return { isAdmin: false, scope: null }; + + const { role: userRole } = user; + if (userRole === UserPermissionRole.ADMIN) return { isAdmin: true, scope: ScopeOfAdmin.SystemWide }; + + const orgOwnerOrAdminMemberships = await prisma.membership.findMany({ + where: { + userId: userId, + accepted: true, + team: { + isOrganization: true, + }, + OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }], + }, + select: { + team: { + select: { + id: true, + isOrganization: true, + }, + }, + }, + }); + if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null }; + + return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin }; }; diff --git a/apps/api/v1/lib/utils/retrieveScopedAccessibleUsers.ts b/apps/api/v1/lib/utils/retrieveScopedAccessibleUsers.ts new file mode 100644 index 0000000000..9e80317de0 --- /dev/null +++ b/apps/api/v1/lib/utils/retrieveScopedAccessibleUsers.ts @@ -0,0 +1,92 @@ +import prisma from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; + +type AccessibleUsersType = { + memberUserIds: number[]; + adminUserId: number; +}; + +const getAllOrganizationMemberships = async ( + memberships: { + userId: number; + role: MembershipRole; + teamId: number; + }[], + orgId: number +) => { + return memberships.reduce((acc, membership) => { + if (membership.teamId === orgId) { + acc.push(membership.userId); + } + return acc; + }, []); +}; + +const getAllAdminMemberships = async (userId: number) => { + return await prisma.membership.findMany({ + where: { + userId: userId, + accepted: true, + OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }], + }, + select: { + team: { + select: { + id: true, + isOrganization: true, + }, + }, + }, + }); +}; + +const getAllOrganizationMembers = async (organizationId: number) => { + return await prisma.membership.findMany({ + where: { + teamId: organizationId, + accepted: true, + }, + select: { + userId: true, + }, + }); +}; + +export const getAccessibleUsers = async ({ + memberUserIds, + adminUserId, +}: AccessibleUsersType): Promise => { + const memberships = await prisma.membership.findMany({ + where: { + team: { + isOrganization: true, + }, + accepted: true, + OR: [ + { userId: { in: memberUserIds } }, + { userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } }, + ], + }, + select: { + userId: true, + role: true, + teamId: true, + }, + }); + + const orgId = memberships.find((membership) => membership.userId === adminUserId)?.teamId; + if (!orgId) return []; + + const allAccessibleMemberUserIds = await getAllOrganizationMemberships(memberships, orgId); + const accessibleUserIds = allAccessibleMemberUserIds.filter((userId) => userId !== adminUserId); + return accessibleUserIds; +}; + +export const retrieveOrgScopedAccessibleUsers = async ({ adminId }: { adminId: number }) => { + const adminMemberships = await getAllAdminMemberships(adminId); + const organizationId = adminMemberships.find((membership) => membership.team.isOrganization)?.team.id; + if (!organizationId) return []; + + const allMemberships = await getAllOrganizationMembers(organizationId); + return allMemberships.map((membership) => membership.userId); +}; diff --git a/apps/api/v1/lib/utils/scopeOfAdmin.ts b/apps/api/v1/lib/utils/scopeOfAdmin.ts new file mode 100644 index 0000000000..ed09856699 --- /dev/null +++ b/apps/api/v1/lib/utils/scopeOfAdmin.ts @@ -0,0 +1,4 @@ +export const ScopeOfAdmin = { + SystemWide: "SystemWide", + OrgOwnerOrAdmin: "OrgOwnerOrAdmin", +} as const; diff --git a/apps/api/v1/next.d.ts b/apps/api/v1/next.d.ts index 5c1be26eb4..a8d7fbeb1d 100644 --- a/apps/api/v1/next.d.ts +++ b/apps/api/v1/next.d.ts @@ -11,7 +11,8 @@ export declare module "next" { method: string; // session: { user: { id: number } }; // query: Partial<{ [key: string]: string | string[] }>; - isAdmin: boolean; + isSystemWideAdmin: boolean; + isOrganizationOwnerOrAdmin: boolean; pagination: { take: number; skip: number }; } } diff --git a/apps/api/v1/pages/api/api-keys/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/api-keys/[id]/_auth-middleware.ts index c0e3fb1438..f76ec117c6 100644 --- a/apps/api/v1/pages/api/api-keys/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/api-keys/[id]/_auth-middleware.ts @@ -6,10 +6,10 @@ import prisma from "@calcom/prisma"; import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; export async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdAsString.parse(req.query); // Admin can check any api key - if (isAdmin) return; + if (isSystemWideAdmin) return; // Check if user can access the api key const apiKey = await prisma.apiKey.findFirst({ where: { id, userId }, diff --git a/apps/api/v1/pages/api/api-keys/_get.ts b/apps/api/v1/pages/api/api-keys/_get.ts index 8f18e3ebae..84d61c3879 100644 --- a/apps/api/v1/pages/api/api-keys/_get.ts +++ b/apps/api/v1/pages/api/api-keys/_get.ts @@ -16,8 +16,8 @@ type CustomNextApiRequest = NextApiRequest & { function handleAdminRequests(req: CustomNextApiRequest) { // To match type safety with runtime if (!hasReqArgs(req)) throw Error("Missing req.args"); - const { userId, isAdmin } = req; - if (isAdmin && req.query.userId) { + const { userId, isSystemWideAdmin } = req; + if (isSystemWideAdmin && req.query.userId) { const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; req.args.where = { userId: { in: userIds } }; @@ -30,8 +30,8 @@ function hasReqArgs(req: CustomNextApiRequest): req is Ensure) { - const { isAdmin } = req; - if (isAdmin) return; + const { isSystemWideAdmin } = req; + if (isSystemWideAdmin) return; const { userId } = req; const { bookingId } = body; if (bookingId) { diff --git a/apps/api/v1/pages/api/attendees/_get.ts b/apps/api/v1/pages/api/attendees/_get.ts index b3ec13db7b..9f1f456766 100644 --- a/apps/api/v1/pages/api/attendees/_get.ts +++ b/apps/api/v1/pages/api/attendees/_get.ts @@ -31,8 +31,8 @@ import { schemaAttendeeReadPublic } from "~/lib/validations/attendee"; * description: No attendees were found */ async function handler(req: NextApiRequest) { - const { userId, isAdmin } = req; - const args: Prisma.AttendeeFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } }; + const { userId, isSystemWideAdmin } = req; + const args: Prisma.AttendeeFindManyArgs = isSystemWideAdmin ? {} : { where: { booking: { userId } } }; const data = await prisma.attendee.findMany(args); const attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee)); if (!attendees) throw new HttpError({ statusCode: 404, message: "No attendees were found" }); diff --git a/apps/api/v1/pages/api/attendees/_post.ts b/apps/api/v1/pages/api/attendees/_post.ts index 610f306282..03ca47a614 100644 --- a/apps/api/v1/pages/api/attendees/_post.ts +++ b/apps/api/v1/pages/api/attendees/_post.ts @@ -52,10 +52,10 @@ import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "~/lib/ * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const body = schemaAttendeeCreateBodyParams.parse(req.body); - if (!isAdmin) { + if (!isSystemWideAdmin) { const userBooking = await prisma.booking.findFirst({ where: { userId, id: body.bookingId }, select: { id: true }, diff --git a/apps/api/v1/pages/api/availabilities/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/availabilities/[id]/_auth-middleware.ts index 406c808511..245f5c9cb0 100644 --- a/apps/api/v1/pages/api/availabilities/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/availabilities/[id]/_auth-middleware.ts @@ -5,10 +5,10 @@ import prisma from "@calcom/prisma"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin, query } = req; + const { userId, isSystemWideAdmin, query } = req; const { id } = schemaQueryIdParseInt.parse(query); /** Admins can skip the ownership verification */ - if (isAdmin) return; + if (isSystemWideAdmin) return; /** * There's a caveat here. If the availability exists but the user doesn't own it, * the user will see a 404 error which may or not be the desired behavior. diff --git a/apps/api/v1/pages/api/availabilities/_post.ts b/apps/api/v1/pages/api/availabilities/_post.ts index a0f915241f..2e0a8e1247 100644 --- a/apps/api/v1/pages/api/availabilities/_post.ts +++ b/apps/api/v1/pages/api/availabilities/_post.ts @@ -86,8 +86,8 @@ async function postHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest) { - const { userId, isAdmin } = req; - if (isAdmin) return; + const { userId, isSystemWideAdmin } = req; + if (isSystemWideAdmin) return; const data = schemaAvailabilityCreateBodyParams.parse(req.body); const schedule = await prisma.schedule.findFirst({ where: { userId, id: data.scheduleId }, diff --git a/apps/api/v1/pages/api/availability/_get.ts b/apps/api/v1/pages/api/availability/_get.ts index 580d149c6f..935619cc19 100644 --- a/apps/api/v1/pages/api/availability/_get.ts +++ b/apps/api/v1/pages/api/availability/_get.ts @@ -189,7 +189,7 @@ const availabilitySchema = z ); async function handler(req: NextApiRequest) { - const { isAdmin, userId: reqUserId } = req; + const { isSystemWideAdmin, userId: reqUserId } = req; const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query); if (!teamId) return getUserAvailability({ @@ -224,7 +224,7 @@ async function handler(req: NextApiRequest) { const isUserAdminOrOwner = memberRoles[reqUserId] == MembershipRole.ADMIN || memberRoles[reqUserId] == MembershipRole.OWNER || - isAdmin; + isSystemWideAdmin; if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" }); const availabilities = members.map(async (user) => { return { diff --git a/apps/api/v1/pages/api/booking-references/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/booking-references/[id]/_auth-middleware.ts index 1970d5d208..542f16265b 100644 --- a/apps/api/v1/pages/api/booking-references/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/booking-references/[id]/_auth-middleware.ts @@ -6,10 +6,10 @@ import prisma from "@calcom/prisma"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdParseInt.parse(req.query); // Here we make sure to only return references of the user's own bookings if the user is not an admin. - if (isAdmin) return; + if (isSystemWideAdmin) return; // Find all references where the user has bookings const bookingReference = await prisma.bookingReference.findFirst({ where: { id, booking: { userId } }, diff --git a/apps/api/v1/pages/api/booking-references/[id]/_patch.ts b/apps/api/v1/pages/api/booking-references/[id]/_patch.ts index d90ddeb31d..b699de7cd9 100644 --- a/apps/api/v1/pages/api/booking-references/[id]/_patch.ts +++ b/apps/api/v1/pages/api/booking-references/[id]/_patch.ts @@ -60,12 +60,12 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform * description: Authorization information is missing or invalid. */ export async function patchHandler(req: NextApiRequest) { - const { query, body, isAdmin, userId } = req; + const { query, body, isSystemWideAdmin, userId } = req; const { id } = schemaQueryIdParseInt.parse(query); const data = schemaBookingEditBodyParams.parse(body); /* If user tries to update bookingId, we run extra checks */ if (data.bookingId) { - const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin + const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin ? /* If admin, we only check that the booking exists */ { where: { id: data.bookingId } } : /* For non-admins we make sure the booking belongs to the user */ diff --git a/apps/api/v1/pages/api/booking-references/_get.ts b/apps/api/v1/pages/api/booking-references/_get.ts index 15dc11c1c0..5794d3fe1d 100644 --- a/apps/api/v1/pages/api/booking-references/_get.ts +++ b/apps/api/v1/pages/api/booking-references/_get.ts @@ -30,8 +30,10 @@ import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-refe * description: No booking references were found */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; - const args: Prisma.BookingReferenceFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } }; + const { userId, isSystemWideAdmin } = req; + const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin + ? {} + : { where: { booking: { userId } } }; const data = await prisma.bookingReference.findMany(args); return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) }; } diff --git a/apps/api/v1/pages/api/booking-references/_post.ts b/apps/api/v1/pages/api/booking-references/_post.ts index 98761421f9..d551ac9890 100644 --- a/apps/api/v1/pages/api/booking-references/_post.ts +++ b/apps/api/v1/pages/api/booking-references/_post.ts @@ -62,9 +62,9 @@ import { * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const body = schemaBookingCreateBodyParams.parse(req.body); - const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin + const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin ? /* If admin, we only check that the booking exists */ { where: { id: body.bookingId } } : /* For non-admins we make sure the booking belongs to the user */ diff --git a/apps/api/v1/pages/api/bookings/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/bookings/[id]/_auth-middleware.ts index b365b675e0..41225f0455 100644 --- a/apps/api/v1/pages/api/bookings/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/bookings/[id]/_auth-middleware.ts @@ -3,15 +3,33 @@ import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; +import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin, query } = req; - if (isAdmin) { + const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin, query } = req; + if (isSystemWideAdmin) { return; } const { id } = schemaQueryIdParseInt.parse(query); + if (isOrganizationOwnerOrAdmin) { + const booking = await prisma.booking.findUnique({ + where: { id }, + select: { userId: true }, + }); + if (booking) { + const bookingUserId = booking.userId; + if (bookingUserId) { + const accessibleUsersIds = await getAccessibleUsers({ + adminUserId: userId, + memberUserIds: [bookingUserId], + }); + if (accessibleUsersIds.length > 0) return; + } + } + } + const userWithBookingsAndTeamIds = await prisma.user.findUnique({ where: { id: userId }, include: { @@ -43,7 +61,7 @@ async function authMiddleware(req: NextApiRequest) { }); if (!teamBookings) { - throw new HttpError({ statusCode: 401, message: "You are not authorized" }); + throw new HttpError({ statusCode: 403, message: "You are not authorized" }); } } } diff --git a/apps/api/v1/pages/api/bookings/[id]/_patch.ts b/apps/api/v1/pages/api/bookings/[id]/_patch.ts index ed36ec8ed4..d26eb3c3b6 100644 --- a/apps/api/v1/pages/api/bookings/[id]/_patch.ts +++ b/apps/api/v1/pages/api/bookings/[id]/_patch.ts @@ -5,6 +5,7 @@ import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; +import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; @@ -109,14 +110,27 @@ export async function patchHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest, body: z.infer) { - const { isAdmin } = req; - if (body.userId && !isAdmin) { + const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req; + if (body.userId && !isSystemWideAdmin && !isOrganizationOwnerOrAdmin) { // Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name throw new HttpError({ statusCode: 403, message: "Only admin can change the organizer of a booking", }); } + + if (body.userId && isOrganizationOwnerOrAdmin) { + const accessibleUsersIds = await getAccessibleUsers({ + adminUserId: userId, + memberUserIds: [body.userId], + }); + if (accessibleUsersIds.length === 0) { + throw new HttpError({ + statusCode: 403, + message: "Only admin can change the organizer of a booking", + }); + } + } } export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/bookings/_get.ts b/apps/api/v1/pages/api/bookings/_get.ts index f732ed7d37..59503a666d 100644 --- a/apps/api/v1/pages/api/bookings/_get.ts +++ b/apps/api/v1/pages/api/bookings/_get.ts @@ -6,6 +6,10 @@ import { defaultResponder } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; import { withMiddleware } from "~/lib/helpers/withMiddleware"; +import { + getAccessibleUsers, + retrieveOrgScopedAccessibleUsers, +} from "~/lib/utils/retrieveScopedAccessibleUsers"; import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking"; import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail"; import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; @@ -109,7 +113,11 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * 404: * description: No bookings were found */ - +type GetAdminArgsType = { + adminDidQueryUserIds?: boolean; + requestedUserIds: number[]; + userId: number; +}; /** * Constructs the WHERE clause for Prisma booking findMany operation. * @@ -162,12 +170,18 @@ function buildWhereClause( }; } -async function handler(req: NextApiRequest) { - const { userId, isAdmin } = req; - +export async function handler(req: NextApiRequest) { + const { + userId, + isSystemWideAdmin, + isOrganizationOwnerOrAdmin, + pagination: { take, skip }, + } = req; const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query); const args: Prisma.BookingFindManyArgs = {}; + args.take = take; + args.skip = skip; args.include = { attendees: true, user: true, @@ -183,19 +197,32 @@ async function handler(req: NextApiRequest) { const filterByAttendeeEmails = attendeeEmails.length > 0; /** Only admins can query other users */ - if (isAdmin) { - if (req.query.userId) { + if (isSystemWideAdmin) { + if (req.query.userId || filterByAttendeeEmails) { const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - const users = await prisma.user.findMany({ - where: { id: { in: userIds } }, - select: { email: true }, - }); - const userEmails = users.map((u) => u.email); - args.where = buildWhereClause(userId, attendeeEmails, userIds, userEmails); - } else if (filterByAttendeeEmails) { - args.where = buildWhereClause(null, attendeeEmails, [], []); + const requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; + + const systemWideAdminArgs = { + adminDidQueryUserIds: !!req.query.userId, + requestedUserIds, + userId, + }; + const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs); + args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails); } + } else if (isOrganizationOwnerOrAdmin) { + let requestedUserIds = [userId]; + if (req.query.userId || filterByAttendeeEmails) { + const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); + requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; + } + const orgWideAdminArgs = { + adminDidQueryUserIds: !!req.query.userId, + requestedUserIds, + userId, + }; + const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs); + args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails); } else { const user = await prisma.user.findUnique({ where: { id: userId }, @@ -204,7 +231,7 @@ async function handler(req: NextApiRequest) { }, }); if (!user) { - throw new HttpError({ message: "User not found", statusCode: 500 }); + throw new HttpError({ message: "User not found", statusCode: 404 }); } args.where = buildWhereClause(userId, attendeeEmails, [], []); } @@ -226,4 +253,53 @@ async function handler(req: NextApiRequest) { return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) }; } +const handleSystemWideAdminArgs = async ({ + adminDidQueryUserIds, + requestedUserIds, + userId, +}: GetAdminArgsType) => { + if (adminDidQueryUserIds) { + const users = await prisma.user.findMany({ + where: { id: { in: requestedUserIds } }, + select: { email: true }, + }); + const userEmails = users.map((u) => u.email); + + return { userId, userIds: requestedUserIds, userEmails }; + } + return { userId: null, userIds: [], userEmails: [] }; +}; + +const handleOrgWideAdminArgs = async ({ + adminDidQueryUserIds, + requestedUserIds, + userId, +}: GetAdminArgsType) => { + if (adminDidQueryUserIds) { + const accessibleUsersIds = await getAccessibleUsers({ + adminUserId: userId, + memberUserIds: requestedUserIds, + }); + + if (!accessibleUsersIds.length) throw new HttpError({ message: "No User found", statusCode: 404 }); + const users = await prisma.user.findMany({ + where: { id: { in: accessibleUsersIds } }, + select: { email: true }, + }); + const userEmails = users.map((u) => u.email); + return { userId, userIds: accessibleUsersIds, userEmails }; + } else { + const accessibleUsersIds = await retrieveOrgScopedAccessibleUsers({ + adminId: userId, + }); + + const users = await prisma.user.findMany({ + where: { id: { in: accessibleUsersIds } }, + select: { email: true }, + }); + const userEmails = users.map((u) => u.email); + return { userId, userIds: accessibleUsersIds, userEmails }; + } +}; + export default withMiddleware("pagination")(defaultResponder(handler)); diff --git a/apps/api/v1/pages/api/bookings/_post.ts b/apps/api/v1/pages/api/bookings/_post.ts index c94fe6c461..9fe55b773a 100644 --- a/apps/api/v1/pages/api/bookings/_post.ts +++ b/apps/api/v1/pages/api/bookings/_post.ts @@ -4,6 +4,8 @@ import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBooking import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking"; import { defaultResponder } from "@calcom/lib/server"; +import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; + /** * @swagger * /bookings: @@ -204,8 +206,17 @@ import { defaultResponder } from "@calcom/lib/server"; * description: Authorization information is missing or invalid. */ async function handler(req: NextApiRequest) { - const { userId, isAdmin } = req; - if (isAdmin) req.userId = req.body.userId || userId; + const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req; + if (isSystemWideAdmin) req.userId = req.body.userId || userId; + + if (isOrganizationOwnerOrAdmin) { + const accessibleUsersIds = await getAccessibleUsers({ + adminUserId: userId, + memberUserIds: [req.body.userId || userId], + }); + const [requestedUserId] = accessibleUsersIds; + req.userId = requestedUserId || userId; + } return await handleNewBooking(req, getBookingDataSchemaForApi); } diff --git a/apps/api/v1/pages/api/connected-calendars/_get.ts b/apps/api/v1/pages/api/connected-calendars/_get.ts index 47085c33ac..a6d5452af0 100644 --- a/apps/api/v1/pages/api/connected-calendars/_get.ts +++ b/apps/api/v1/pages/api/connected-calendars/_get.ts @@ -97,9 +97,10 @@ import { schemaConnectedCalendarsReadPublic } from "~/lib/validations/connected- */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; - if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" }); + if (!isSystemWideAdmin && req.query.userId) + throw new HttpError({ statusCode: 403, message: "ADMIN required" }); const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId]; diff --git a/apps/api/v1/pages/api/custom-inputs/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/custom-inputs/[id]/_auth-middleware.ts index bc5888acbb..05243a3922 100644 --- a/apps/api/v1/pages/api/custom-inputs/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/custom-inputs/[id]/_auth-middleware.ts @@ -6,10 +6,10 @@ import prisma from "@calcom/prisma"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdParseInt.parse(req.query); // Admins can just skip this check - if (isAdmin) return; + if (isSystemWideAdmin) return; // Check if the current user can access the event type of this input const eventTypeCustomInput = await prisma.eventTypeCustomInput.findFirst({ where: { id, eventType: { userId } }, diff --git a/apps/api/v1/pages/api/custom-inputs/_get.ts b/apps/api/v1/pages/api/custom-inputs/_get.ts index 02e8909451..00259fa649 100644 --- a/apps/api/v1/pages/api/custom-inputs/_get.ts +++ b/apps/api/v1/pages/api/custom-inputs/_get.ts @@ -29,8 +29,10 @@ import { schemaEventTypeCustomInputPublic } from "~/lib/validations/event-type-c * description: No eventTypeCustomInputs were found */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; - const args: Prisma.EventTypeCustomInputFindManyArgs = isAdmin ? {} : { where: { eventType: { userId } } }; + const { userId, isSystemWideAdmin } = req; + const args: Prisma.EventTypeCustomInputFindManyArgs = isSystemWideAdmin + ? {} + : { where: { eventType: { userId } } }; const data = await prisma.eventTypeCustomInput.findMany(args); return { event_type_custom_inputs: data.map((v) => schemaEventTypeCustomInputPublic.parse(v)) }; } diff --git a/apps/api/v1/pages/api/custom-inputs/_post.ts b/apps/api/v1/pages/api/custom-inputs/_post.ts index cd6e04a698..3c01c416de 100644 --- a/apps/api/v1/pages/api/custom-inputs/_post.ts +++ b/apps/api/v1/pages/api/custom-inputs/_post.ts @@ -80,10 +80,10 @@ import { * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { eventTypeId, ...body } = schemaEventTypeCustomInputBodyParams.parse(req.body); - if (!isAdmin) { + if (!isSystemWideAdmin) { /* We check that the user has access to the event type he's trying to add a custom input to. */ const eventType = await prisma.eventType.findFirst({ where: { id: eventTypeId, userId }, diff --git a/apps/api/v1/pages/api/destination-calendars/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/destination-calendars/[id]/_auth-middleware.ts index 276cf44f44..7878b05b91 100644 --- a/apps/api/v1/pages/api/destination-calendars/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/destination-calendars/[id]/_auth-middleware.ts @@ -6,9 +6,9 @@ import prisma from "@calcom/prisma"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdParseInt.parse(req.query); - if (isAdmin) return; + if (isSystemWideAdmin) return; const userEventTypes = await prisma.eventType.findMany({ where: { userId }, select: { id: true }, diff --git a/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts b/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts index 75221c5192..064634b2f2 100644 --- a/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts +++ b/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts @@ -83,10 +83,10 @@ type UserCredentialType = { }; export async function patchHandler(req: NextApiRequest) { - const { userId, isAdmin, query, body } = req; + const { userId, isSystemWideAdmin, query, body } = req; const { id } = schemaQueryIdParseInt.parse(query); const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body); - const assignedUserId = isAdmin ? parsedBody.userId || userId : userId; + const assignedUserId = isSystemWideAdmin ? parsedBody.userId || userId : userId; validateIntegrationInput(parsedBody); const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma); diff --git a/apps/api/v1/pages/api/destination-calendars/_post.ts b/apps/api/v1/pages/api/destination-calendars/_post.ts index e8fdf266b2..1d8379335c 100644 --- a/apps/api/v1/pages/api/destination-calendars/_post.ts +++ b/apps/api/v1/pages/api/destination-calendars/_post.ts @@ -61,11 +61,11 @@ import { * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin, body } = req; + const { userId, isSystemWideAdmin, body } = req; const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body); await checkPermissions(req, userId); - const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.userId : userId; + const assignedUserId = isSystemWideAdmin && parsedBody.userId ? parsedBody.userId : userId; /* Check if credentialId data matches the ownership and integration passed in */ const userCredentials = await prisma.credential.findMany({ @@ -120,19 +120,20 @@ async function postHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest, userId: number) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; const body = schemaDestinationCalendarCreateBodyParams.parse(req.body); /* Non-admin users can only create destination calendars for themselves */ - if (!isAdmin && body.userId) + if (!isSystemWideAdmin && body.userId) throw new HttpError({ statusCode: 401, message: "ADMIN required for `userId`", }); /* Admin users are required to pass in a userId */ - if (isAdmin && !body.userId) throw new HttpError({ statusCode: 400, message: "`userId` required" }); + if (isSystemWideAdmin && !body.userId) + throw new HttpError({ statusCode: 400, message: "`userId` required" }); /* User should only be able to create for their own destination calendars*/ - if (!isAdmin && body.eventTypeId) { + if (!isSystemWideAdmin && body.eventTypeId) { const ownsEventType = await prisma.eventType.findFirst({ where: { id: body.eventTypeId, userId } }); if (!ownsEventType) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); } diff --git a/apps/api/v1/pages/api/event-types/[id]/_delete.ts b/apps/api/v1/pages/api/event-types/[id]/_delete.ts index 519047a127..40a90db5f3 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_delete.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_delete.ts @@ -46,9 +46,9 @@ export async function deleteHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdParseInt.parse(req.query); - if (isAdmin) return; + if (isSystemWideAdmin) return; /** Only event type owners can delete it */ const eventType = await prisma.eventType.findFirst({ where: { id, userId } }); if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" }); diff --git a/apps/api/v1/pages/api/event-types/[id]/_get.ts b/apps/api/v1/pages/api/event-types/[id]/_get.ts index e21c8bbe0f..6410a47b0f 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_get.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_get.ts @@ -87,7 +87,7 @@ async function checkPermissions( req: NextApiRequest, eventType: (T & Partial>) | null ) { - if (req.isAdmin) return true; + if (req.isSystemWideAdmin) return true; if (eventType?.teamId) { req.query.teamId = String(eventType.teamId); await canAccessTeamEventOrThrow(req, { diff --git a/apps/api/v1/pages/api/event-types/[id]/_patch.ts b/apps/api/v1/pages/api/event-types/[id]/_patch.ts index 68b270324b..1e17c21d31 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_patch.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_patch.ts @@ -237,9 +237,9 @@ export async function patchHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest, body: z.infer) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdParseInt.parse(req.query); - if (isAdmin) return; + if (isSystemWideAdmin) return; /** Only event type owners can modify it */ const eventType = await prisma.eventType.findFirst({ where: { id, userId } }); if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" }); diff --git a/apps/api/v1/pages/api/event-types/_get.ts b/apps/api/v1/pages/api/event-types/_get.ts index e57926d66e..1db594350a 100644 --- a/apps/api/v1/pages/api/event-types/_get.ts +++ b/apps/api/v1/pages/api/event-types/_get.ts @@ -43,10 +43,10 @@ import getCalLink from "./_utils/getCalLink"; * description: No event types were found */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId]; const { slug } = schemaQuerySlug.parse(req.query); - const shouldUseUserId = !isAdmin || !slug || !!req.query.userId; + const shouldUseUserId = !isSystemWideAdmin || !slug || !!req.query.userId; // When user is admin and no query params are provided we should return all event types. // But currently we return only the event types of the user. Not changing this for backwards compatibility. const data = await prisma.eventType.findMany({ @@ -74,9 +74,9 @@ async function getHandler(req: NextApiRequest) { }; } // TODO: Extract & reuse. -function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) { +function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) { /** Guard: Only admins can query other users */ - if (!isAdmin) { + if (!isSystemWideAdmin) { throw new HttpError({ statusCode: 401, message: "ADMIN required" }); } const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query); diff --git a/apps/api/v1/pages/api/event-types/_post.ts b/apps/api/v1/pages/api/event-types/_post.ts index 19aec7ca33..9f196385d2 100644 --- a/apps/api/v1/pages/api/event-types/_post.ts +++ b/apps/api/v1/pages/api/event-types/_post.ts @@ -265,7 +265,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts"; * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin, body } = req; + const { userId, isSystemWideAdmin, body } = req; const { hosts = [], @@ -291,7 +291,7 @@ async function postHandler(req: NextApiRequest) { await checkUserMembership(req); } - if (isAdmin && parsedBody.userId) { + if (isSystemWideAdmin && parsedBody.userId) { data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } }; } @@ -311,18 +311,18 @@ async function postHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; const body = schemaEventTypeCreateBodyParams.parse(req.body); /* Non-admin users can only create event types for themselves */ - if (!isAdmin && body.userId) + if (!isSystemWideAdmin && body.userId) throw new HttpError({ statusCode: 401, message: "ADMIN required for `userId`", }); if ( body.teamId && - !isAdmin && - !(await canUserAccessTeamWithRole(req.userId, isAdmin, body.teamId, { + !isSystemWideAdmin && + !(await canUserAccessTeamWithRole(req.userId, isSystemWideAdmin, body.teamId, { in: [MembershipRole.OWNER, MembershipRole.ADMIN], })) ) @@ -331,7 +331,7 @@ async function checkPermissions(req: NextApiRequest) { message: "ADMIN required for `teamId`", }); /* Admin users are required to pass in a userId or teamId */ - if (isAdmin && !body.userId && !body.teamId) + if (isSystemWideAdmin && !body.userId && !body.teamId) throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" }); } diff --git a/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts b/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts index 1055dc0d32..76c7cba3a7 100644 --- a/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts +++ b/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts @@ -10,9 +10,9 @@ export default async function checkTeamEventEditPermission( req: NextApiRequest, body: Pick, "teamId" | "userId"> ) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; let userId = req.userId; - if (isAdmin && body.userId) { + if (isSystemWideAdmin && body.userId) { userId = body.userId; } if (body.teamId) { diff --git a/apps/api/v1/pages/api/invites/_post.ts b/apps/api/v1/pages/api/invites/_post.ts index 94703111a3..4fe81569bf 100644 --- a/apps/api/v1/pages/api/invites/_post.ts +++ b/apps/api/v1/pages/api/invites/_post.ts @@ -58,10 +58,10 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) { } async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) { - const { userId, isAdmin } = req; - if (isAdmin) return; + const { userId, isSystemWideAdmin } = req; + if (isSystemWideAdmin) return; // To prevent auto-accepted invites, limit it to ADMIN users - if (!isAdmin && "accepted" in body) + if (!isSystemWideAdmin && "accepted" in body) throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" }); // Only team OWNERS and ADMINS can add other members const membership = await prisma.membership.findFirst({ diff --git a/apps/api/v1/pages/api/memberships/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/memberships/[id]/_auth-middleware.ts index e5bb676583..f14317eae3 100644 --- a/apps/api/v1/pages/api/memberships/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/memberships/[id]/_auth-middleware.ts @@ -6,10 +6,10 @@ import prisma from "@calcom/prisma"; import { membershipIdSchema } from "~/lib/validations/membership"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { teamId } = membershipIdSchema.parse(req.query); // Admins can just skip this check - if (isAdmin) return; + if (isSystemWideAdmin) return; // Only team members can modify a membership const membership = await prisma.membership.findFirst({ where: { userId, teamId } }); if (!membership) throw new HttpError({ statusCode: 403, message: "Forbidden" }); diff --git a/apps/api/v1/pages/api/memberships/[id]/_delete.ts b/apps/api/v1/pages/api/memberships/[id]/_delete.ts index 1e624f3adc..e6c39eb482 100644 --- a/apps/api/v1/pages/api/memberships/[id]/_delete.ts +++ b/apps/api/v1/pages/api/memberships/[id]/_delete.ts @@ -43,10 +43,10 @@ export async function deleteHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest) { - const { isAdmin, userId, query } = req; + const { isSystemWideAdmin, userId, query } = req; const userId_teamId = membershipIdSchema.parse(query); // Admin User can do anything including deletion of Admin Team Member in any team - if (isAdmin) { + if (isSystemWideAdmin) { return; } diff --git a/apps/api/v1/pages/api/memberships/[id]/_patch.ts b/apps/api/v1/pages/api/memberships/[id]/_patch.ts index 3e7dcaffb9..f44e573922 100644 --- a/apps/api/v1/pages/api/memberships/[id]/_patch.ts +++ b/apps/api/v1/pages/api/memberships/[id]/_patch.ts @@ -52,11 +52,11 @@ export async function patchHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { userId: queryUserId, teamId } = membershipIdSchema.parse(req.query); const data = membershipEditBodySchema.parse(req.body); // Admins can just skip this check - if (isAdmin) return; + if (isSystemWideAdmin) return; // Only the invited user can accept the invite if ("accepted" in data && queryUserId !== userId) throw new HttpError({ diff --git a/apps/api/v1/pages/api/memberships/_get.ts b/apps/api/v1/pages/api/memberships/_get.ts index 8a42a5dd3a..c175718a8b 100644 --- a/apps/api/v1/pages/api/memberships/_get.ts +++ b/apps/api/v1/pages/api/memberships/_get.ts @@ -46,26 +46,28 @@ async function getHandler(req: NextApiRequest) { * Returns requested users IDs only if admin, otherwise return only current user ID */ function getUserIds(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; /** Only admins can query other users */ - if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - if (isAdmin && req.query.userId) { + if (!isSystemWideAdmin && req.query.userId) + throw new HttpError({ statusCode: 403, message: "ADMIN required" }); + if (isSystemWideAdmin && req.query.userId) { const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; return userIds; } // Return all memberships for ADMIN, limit to current user to non-admins - return isAdmin ? undefined : [userId]; + return isSystemWideAdmin ? undefined : [userId]; } /** * Returns requested teams IDs only if admin */ function getTeamIds(req: NextApiRequest) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; /** Only admins can query other teams */ - if (!isAdmin && req.query.teamId) throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - if (isAdmin && req.query.teamId) { + if (!isSystemWideAdmin && req.query.teamId) + throw new HttpError({ statusCode: 403, message: "ADMIN required" }); + if (isSystemWideAdmin && req.query.teamId) { const query = schemaQuerySingleOrMultipleTeamIds.parse(req.query); const teamIds = Array.isArray(query.teamId) ? query.teamId : [query.teamId]; return teamIds; diff --git a/apps/api/v1/pages/api/memberships/_post.ts b/apps/api/v1/pages/api/memberships/_post.ts index e1ee1a622c..9c4b1af12e 100644 --- a/apps/api/v1/pages/api/memberships/_post.ts +++ b/apps/api/v1/pages/api/memberships/_post.ts @@ -37,11 +37,11 @@ async function postHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest) { - const { userId, isAdmin } = req; - if (isAdmin) return; + const { userId, isSystemWideAdmin } = req; + if (isSystemWideAdmin) return; const body = membershipCreateBodySchema.parse(req.body); // To prevent auto-accepted invites, limit it to ADMIN users - if (!isAdmin && "accepted" in body) + if (!isSystemWideAdmin && "accepted" in body) throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" }); // Only team OWNERS and ADMINS can add other members const membership = await prisma.membership.findFirst({ diff --git a/apps/api/v1/pages/api/schedules/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/schedules/[id]/_auth-middleware.ts index d622dfcbf6..ef44111c2e 100644 --- a/apps/api/v1/pages/api/schedules/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/schedules/[id]/_auth-middleware.ts @@ -6,10 +6,10 @@ import prisma from "@calcom/prisma"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdParseInt.parse(req.query); // Admins can just skip this check - if (isAdmin) return; + if (isSystemWideAdmin) return; // Check if the current user can access the schedule const schedule = await prisma.schedule.findFirst({ where: { id, userId }, diff --git a/apps/api/v1/pages/api/schedules/[id]/_patch.ts b/apps/api/v1/pages/api/schedules/[id]/_patch.ts index f9009e30bf..c9236eff84 100644 --- a/apps/api/v1/pages/api/schedules/[id]/_patch.ts +++ b/apps/api/v1/pages/api/schedules/[id]/_patch.ts @@ -91,8 +91,8 @@ export async function patchHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest, body: z.infer) { - const { isAdmin } = req; - if (isAdmin) return; + const { isSystemWideAdmin } = req; + if (isSystemWideAdmin) return; if (body.userId) { throw new HttpError({ statusCode: 403, message: "Non admin cannot change the owner of a schedule" }); } diff --git a/apps/api/v1/pages/api/schedules/_get.ts b/apps/api/v1/pages/api/schedules/_get.ts index bbe893516e..d70c64aeca 100644 --- a/apps/api/v1/pages/api/schedules/_get.ts +++ b/apps/api/v1/pages/api/schedules/_get.ts @@ -77,17 +77,17 @@ export const schemaUserIds = z */ async function handler(req: NextApiRequest) { - const { userId, isAdmin } = req; - const args: Prisma.ScheduleFindManyArgs = isAdmin ? {} : { where: { userId } }; + const { userId, isSystemWideAdmin } = req; + const args: Prisma.ScheduleFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } }; args.include = { availability: true }; - if (!isAdmin && req.query.userId) + if (!isSystemWideAdmin && req.query.userId) throw new HttpError({ statusCode: 401, message: "Unauthorized: Only admins can query other users", }); - if (isAdmin && req.query.userId) { + if (isSystemWideAdmin && req.query.userId) { const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; args.where = { userId: { in: userIds } }; diff --git a/apps/api/v1/pages/api/schedules/_post.ts b/apps/api/v1/pages/api/schedules/_post.ts index 05215e3d47..8b8de20472 100644 --- a/apps/api/v1/pages/api/schedules/_post.ts +++ b/apps/api/v1/pages/api/schedules/_post.ts @@ -80,14 +80,14 @@ import { schemaCreateScheduleBodyParams, schemaSchedulePublic } from "~/lib/vali */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const body = schemaCreateScheduleBodyParams.parse(req.body); let args: Prisma.ScheduleCreateArgs = { data: { ...body, userId } }; /* If ADMIN we create the schedule for selected user */ - if (isAdmin && body.userId) args = { data: { ...body, userId: body.userId } }; + if (isSystemWideAdmin && body.userId) args = { data: { ...body, userId: body.userId } }; - if (!isAdmin && body.userId) + if (!isSystemWideAdmin && body.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required for `userId`" }); // We create default availabilities for the schedule diff --git a/apps/api/v1/pages/api/selected-calendars/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/selected-calendars/[id]/_auth-middleware.ts index 620ba818fb..09ce0c3940 100644 --- a/apps/api/v1/pages/api/selected-calendars/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/selected-calendars/[id]/_auth-middleware.ts @@ -5,10 +5,10 @@ import { HttpError } from "@calcom/lib/http-error"; import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { userId: queryUserId } = selectedCalendarIdSchema.parse(req.query); // Admins can just skip this check - if (isAdmin) return; + if (isSystemWideAdmin) return; // Check if the current user requesting is the same as the one being requested if (userId !== queryUserId) throw new HttpError({ statusCode: 403, message: "Forbidden" }); } diff --git a/apps/api/v1/pages/api/selected-calendars/[id]/_patch.ts b/apps/api/v1/pages/api/selected-calendars/[id]/_patch.ts index 84b60d1210..602e85b8ff 100644 --- a/apps/api/v1/pages/api/selected-calendars/[id]/_patch.ts +++ b/apps/api/v1/pages/api/selected-calendars/[id]/_patch.ts @@ -53,14 +53,15 @@ import { * description: Authorization information is missing or invalid. */ export async function patchHandler(req: NextApiRequest) { - const { query, isAdmin } = req; + const { query, isSystemWideAdmin } = req; const userId_integration_externalId = selectedCalendarIdSchema.parse(query); const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body); const args: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data }; - if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); + if (!isSystemWideAdmin && bodyUserId) + throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - if (isAdmin && bodyUserId) { + if (isSystemWideAdmin && bodyUserId) { const where: Prisma.UserWhereInput = { id: bodyUserId }; await prisma.user.findFirstOrThrow({ where }); args.data.userId = bodyUserId; diff --git a/apps/api/v1/pages/api/selected-calendars/_get.ts b/apps/api/v1/pages/api/selected-calendars/_get.ts index 1d4bdf9d6b..254d3428e2 100644 --- a/apps/api/v1/pages/api/selected-calendars/_get.ts +++ b/apps/api/v1/pages/api/selected-calendars/_get.ts @@ -32,13 +32,14 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * description: No selected calendars were found */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; /* Admin gets all selected calendar by default, otherwise only the user's ones */ - const args: Prisma.SelectedCalendarFindManyArgs = isAdmin ? {} : { where: { userId } }; + const args: Prisma.SelectedCalendarFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } }; /** Only admins can query other users */ - if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - if (isAdmin && req.query.userId) { + if (!isSystemWideAdmin && req.query.userId) + throw new HttpError({ statusCode: 403, message: "ADMIN required" }); + if (isSystemWideAdmin && req.query.userId) { const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; args.where = { userId: { in: userIds } }; diff --git a/apps/api/v1/pages/api/selected-calendars/_post.ts b/apps/api/v1/pages/api/selected-calendars/_post.ts index d0509df9c1..5d0a419883 100644 --- a/apps/api/v1/pages/api/selected-calendars/_post.ts +++ b/apps/api/v1/pages/api/selected-calendars/_post.ts @@ -50,13 +50,14 @@ import { * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body); const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } }; - if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); + if (!isSystemWideAdmin && bodyUserId) + throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - if (isAdmin && bodyUserId) { + if (isSystemWideAdmin && bodyUserId) { const where: Prisma.UserWhereInput = { id: bodyUserId }; await prisma.user.findFirstOrThrow({ where }); args.data.userId = bodyUserId; diff --git a/apps/api/v1/pages/api/teams/[teamId]/_auth-middleware.ts b/apps/api/v1/pages/api/teams/[teamId]/_auth-middleware.ts index 693eb544a5..f98cfbef8a 100644 --- a/apps/api/v1/pages/api/teams/[teamId]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/teams/[teamId]/_auth-middleware.ts @@ -8,10 +8,10 @@ import { MembershipRole } from "@calcom/prisma/enums"; import { schemaQueryTeamId } from "~/lib/validations/shared/queryTeamId"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { teamId } = schemaQueryTeamId.parse(req.query); /** Admins can skip the ownership verification */ - if (isAdmin) return; + if (isSystemWideAdmin) return; /** Non-members will see a 404 error which may or not be the desired behavior. */ await prisma.team.findFirstOrThrow({ where: { id: teamId, members: { some: { userId } } }, @@ -22,24 +22,24 @@ export async function checkPermissions( req: NextApiRequest, role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER ) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { teamId } = schemaQueryTeamId.parse({ teamId: req.query.teamId, version: req.query.version, apiKey: req.query.apiKey, }); - return canUserAccessTeamWithRole(userId, isAdmin, teamId, role); + return canUserAccessTeamWithRole(userId, isSystemWideAdmin, teamId, role); } export async function canUserAccessTeamWithRole( userId: number, - isAdmin: boolean, + isSystemWideAdmin: boolean, teamId: number, role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER ) { const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } }; /** If not ADMIN then we check if the actual user belongs to team and matches the required role */ - if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } }; + if (!isSystemWideAdmin) args.where = { ...args.where, members: { some: { userId, role } } }; const team = await prisma.team.findFirst(args); if (!team) throw new HttpError({ statusCode: 401, message: `Unauthorized: ${role.toString()} required` }); return team; diff --git a/apps/api/v1/pages/api/teams/[teamId]/_get.ts b/apps/api/v1/pages/api/teams/[teamId]/_get.ts index 829a4104b6..2e76910f9e 100644 --- a/apps/api/v1/pages/api/teams/[teamId]/_get.ts +++ b/apps/api/v1/pages/api/teams/[teamId]/_get.ts @@ -37,11 +37,11 @@ import { schemaTeamReadPublic } from "~/lib/validations/team"; * description: Team was not found */ export async function getHandler(req: NextApiRequest) { - const { isAdmin, userId } = req; + const { isSystemWideAdmin, userId } = req; const { teamId } = schemaQueryTeamId.parse(req.query); const where: Prisma.TeamWhereInput = { id: teamId }; // Non-admins can only query the teams they're part of - if (!isAdmin) where.members = { some: { userId } }; + if (!isSystemWideAdmin) where.members = { some: { userId } }; const data = await prisma.team.findFirstOrThrow({ where }); return { team: schemaTeamReadPublic.parse(data) }; } diff --git a/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts b/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts index 5ef33379c0..bd17989b19 100644 --- a/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts +++ b/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts @@ -42,13 +42,13 @@ const querySchema = z.object({ * description: No event types were found */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { teamId } = querySchema.parse(req.query); const args: Prisma.EventTypeFindManyArgs = { where: { - team: isAdmin + team: isSystemWideAdmin ? { id: teamId, } diff --git a/apps/api/v1/pages/api/teams/[teamId]/publish.ts b/apps/api/v1/pages/api/teams/[teamId]/publish.ts index 781a42387c..d23eb57b38 100644 --- a/apps/api/v1/pages/api/teams/[teamId]/publish.ts +++ b/apps/api/v1/pages/api/teams/[teamId]/publish.ts @@ -21,7 +21,7 @@ const patchHandler = async (req: NextApiRequest, res: NextApiResponse) => { user: { id: req.userId, username: "" /* Not used in this context */, - role: req.isAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER, + role: req.isSystemWideAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER, profile: { id: null, organizationId: null, diff --git a/apps/api/v1/pages/api/teams/_get.ts b/apps/api/v1/pages/api/teams/_get.ts index c5ebe8cf08..4b28a07b39 100644 --- a/apps/api/v1/pages/api/teams/_get.ts +++ b/apps/api/v1/pages/api/teams/_get.ts @@ -30,10 +30,10 @@ import { schemaTeamsReadPublic } from "~/lib/validations/team"; * description: No teams were found */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const where: Prisma.TeamWhereInput = {}; // If user is not ADMIN, return only his data. - if (!isAdmin) where.members = { some: { userId } }; + if (!isSystemWideAdmin) where.members = { some: { userId } }; const data = await prisma.team.findMany({ where }); return { teams: schemaTeamsReadPublic.parse(data) }; } diff --git a/apps/api/v1/pages/api/teams/_post.ts b/apps/api/v1/pages/api/teams/_post.ts index 8fcc00fe22..32722a4602 100644 --- a/apps/api/v1/pages/api/teams/_post.ts +++ b/apps/api/v1/pages/api/teams/_post.ts @@ -80,12 +80,12 @@ import { schemaTeamCreateBodyParams, schemaTeamReadPublic } from "~/lib/validati * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { body, userId, isAdmin } = req; + const { body, userId, isSystemWideAdmin } = req; const { ownerId, ...data } = schemaTeamCreateBodyParams.parse(body); await checkPermissions(req); - const effectiveUserId = isAdmin && ownerId ? ownerId : userId; + const effectiveUserId = isSystemWideAdmin && ownerId ? ownerId : userId; if (data.slug) { const alreadyExist = await prisma.team.findFirst({ @@ -162,11 +162,11 @@ async function postHandler(req: NextApiRequest) { } async function checkPermissions(req: NextApiRequest) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; const body = schemaTeamCreateBodyParams.parse(req.body); /* Non-admin users can only create teams for themselves */ - if (!isAdmin && body.ownerId) + if (!isSystemWideAdmin && body.ownerId) throw new HttpError({ statusCode: 401, message: "ADMIN required for `ownerId`", diff --git a/apps/api/v1/pages/api/users/[userId]/_delete.ts b/apps/api/v1/pages/api/users/[userId]/_delete.ts index 90d38aad36..436c1fdf0e 100644 --- a/apps/api/v1/pages/api/users/[userId]/_delete.ts +++ b/apps/api/v1/pages/api/users/[userId]/_delete.ts @@ -38,10 +38,11 @@ import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId"; * description: Authorization information is missing or invalid. */ export async function deleteHandler(req: NextApiRequest) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; const query = schemaQueryUserId.parse(req.query); // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user - if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" }); + if (!isSystemWideAdmin && query.userId !== req.userId) + throw new HttpError({ statusCode: 403, message: "Forbidden" }); const user = await prisma.user.findUnique({ where: { id: query.userId }, diff --git a/apps/api/v1/pages/api/users/[userId]/_get.ts b/apps/api/v1/pages/api/users/[userId]/_get.ts index 215cf81735..43dba1e4c1 100644 --- a/apps/api/v1/pages/api/users/[userId]/_get.ts +++ b/apps/api/v1/pages/api/users/[userId]/_get.ts @@ -38,11 +38,12 @@ import { schemaUserReadPublic } from "~/lib/validations/user"; * description: User was not found */ export async function getHandler(req: NextApiRequest) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; const query = schemaQueryUserId.parse(req.query); // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user - if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" }); + if (!isSystemWideAdmin && query.userId !== req.userId) + throw new HttpError({ statusCode: 403, message: "Forbidden" }); const data = await prisma.user.findUnique({ where: { id: query.userId } }); const user = schemaUserReadPublic.parse(data); return { user }; diff --git a/apps/api/v1/pages/api/users/[userId]/_patch.ts b/apps/api/v1/pages/api/users/[userId]/_patch.ts index e622a43114..a69aa29f0e 100644 --- a/apps/api/v1/pages/api/users/[userId]/_patch.ts +++ b/apps/api/v1/pages/api/users/[userId]/_patch.ts @@ -95,14 +95,15 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation * description: Insufficient permissions to access resource. */ export async function patchHandler(req: NextApiRequest) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; const query = schemaQueryUserId.parse(req.query); // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user - if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" }); + if (!isSystemWideAdmin && query.userId !== req.userId) + throw new HttpError({ statusCode: 403, message: "Forbidden" }); const body = await schemaUserEditBodyParams.parseAsync(req.body); // disable role or branding changes unless admin. - if (!isAdmin) { + if (!isSystemWideAdmin) { if (body.role) body.role = undefined; if (body.hideBranding) body.hideBranding = undefined; } diff --git a/apps/api/v1/pages/api/users/_get.ts b/apps/api/v1/pages/api/users/_get.ts index dcb26d70b6..81761e90cc 100644 --- a/apps/api/v1/pages/api/users/_get.ts +++ b/apps/api/v1/pages/api/users/_get.ts @@ -45,12 +45,12 @@ import { schemaUsersReadPublic } from "~/lib/validations/user"; export async function getHandler(req: NextApiRequest) { const { userId, - isAdmin, + isSystemWideAdmin, pagination: { take, skip }, } = req; const where: Prisma.UserWhereInput = {}; // If user is not ADMIN, return only his data. - if (!isAdmin) where.id = userId; + if (!isSystemWideAdmin) where.id = userId; if (req.query.email) { const validationResult = schemaQuerySingleOrMultipleUserEmails.parse(req.query); diff --git a/apps/api/v1/pages/api/users/_post.ts b/apps/api/v1/pages/api/users/_post.ts index 9b23ebeca5..a8e3fbfae9 100644 --- a/apps/api/v1/pages/api/users/_post.ts +++ b/apps/api/v1/pages/api/users/_post.ts @@ -88,9 +88,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user"; * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { isAdmin } = req; + const { isSystemWideAdmin } = req; // If user is not ADMIN, return unauthorized. - if (!isAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" }); + if (!isSystemWideAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" }); const data = await schemaUserCreateBodyParams.parseAsync(req.body); const user = await prisma.user.create({ data }); req.statusCode = 201; diff --git a/apps/api/v1/pages/api/webhooks/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/webhooks/[id]/_auth-middleware.ts index ce45765eaa..5598f3ed8b 100644 --- a/apps/api/v1/pages/api/webhooks/[id]/_auth-middleware.ts +++ b/apps/api/v1/pages/api/webhooks/[id]/_auth-middleware.ts @@ -6,10 +6,10 @@ import prisma from "@calcom/prisma"; import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdAsString.parse(req.query); // Admins can just skip this check - if (isAdmin) return; + if (isSystemWideAdmin) return; // Check if the current user can access the webhook const webhook = await prisma.webhook.findFirst({ where: { id, appId: null, OR: [{ userId }, { eventType: { team: { members: { some: { userId } } } } }] }, diff --git a/apps/api/v1/pages/api/webhooks/[id]/_patch.ts b/apps/api/v1/pages/api/webhooks/[id]/_patch.ts index f41f775150..7d749b4a61 100644 --- a/apps/api/v1/pages/api/webhooks/[id]/_patch.ts +++ b/apps/api/v1/pages/api/webhooks/[id]/_patch.ts @@ -68,7 +68,7 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali * description: Authorization information is missing or invalid. */ export async function patchHandler(req: NextApiRequest) { - const { query, userId, isAdmin } = req; + const { query, userId, isSystemWideAdmin } = req; const { id } = schemaQueryIdAsString.parse(query); const { eventTypeId, @@ -80,14 +80,15 @@ export async function patchHandler(req: NextApiRequest) { if (eventTypeId) { const where: Prisma.EventTypeWhereInput = { id: eventTypeId }; - if (!isAdmin) where.userId = userId; + if (!isSystemWideAdmin) where.userId = userId; await prisma.eventType.findFirstOrThrow({ where }); args.data.eventTypeId = eventTypeId; } - if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); + if (!isSystemWideAdmin && bodyUserId) + throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - if (isAdmin && bodyUserId) { + if (isSystemWideAdmin && bodyUserId) { const where: Prisma.UserWhereInput = { id: bodyUserId }; await prisma.user.findFirstOrThrow({ where }); args.data.userId = bodyUserId; diff --git a/apps/api/v1/pages/api/webhooks/_get.ts b/apps/api/v1/pages/api/webhooks/_get.ts index 79b712e742..1d1e5b02dc 100644 --- a/apps/api/v1/pages/api/webhooks/_get.ts +++ b/apps/api/v1/pages/api/webhooks/_get.ts @@ -34,14 +34,15 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook"; * description: No webhooks were found */ async function getHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; - const args: Prisma.WebhookFindManyArgs = isAdmin + const { userId, isSystemWideAdmin } = req; + const args: Prisma.WebhookFindManyArgs = isSystemWideAdmin ? {} : { where: { OR: [{ eventType: { userId } }, { userId }] } }; /** Only admins can query other users */ - if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - if (isAdmin && req.query.userId) { + if (!isSystemWideAdmin && req.query.userId) + throw new HttpError({ statusCode: 403, message: "ADMIN required" }); + if (isSystemWideAdmin && req.query.userId) { const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; args.where = { OR: [{ eventType: { userId: { in: userIds } } }, { userId: { in: userIds } }] }; diff --git a/apps/api/v1/pages/api/webhooks/_post.ts b/apps/api/v1/pages/api/webhooks/_post.ts index 36e470e0c9..29a78cf580 100644 --- a/apps/api/v1/pages/api/webhooks/_post.ts +++ b/apps/api/v1/pages/api/webhooks/_post.ts @@ -66,7 +66,7 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin } = req; + const { userId, isSystemWideAdmin } = req; const { eventTypeId, userId: bodyUserId, @@ -80,14 +80,15 @@ async function postHandler(req: NextApiRequest) { if (eventTypeId) { const where: Prisma.EventTypeWhereInput = { id: eventTypeId }; - if (!isAdmin) where.userId = userId; + if (!isSystemWideAdmin) where.userId = userId; await prisma.eventType.findFirstOrThrow({ where }); args.data.eventTypeId = eventTypeId; } - if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); + if (!isSystemWideAdmin && bodyUserId) + throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - if (isAdmin && bodyUserId) { + if (isSystemWideAdmin && bodyUserId) { const where: Prisma.UserWhereInput = { id: bodyUserId }; await prisma.user.findFirstOrThrow({ where }); args.data.userId = bodyUserId; diff --git a/apps/api/v1/test/lib/bookings/[id]/_patch.integration-test.ts b/apps/api/v1/test/lib/bookings/[id]/_patch.integration-test.ts new file mode 100644 index 0000000000..5554d6411d --- /dev/null +++ b/apps/api/v1/test/lib/bookings/[id]/_patch.integration-test.ts @@ -0,0 +1,87 @@ +import type { Request, Response } from "express"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { createMocks } from "node-mocks-http"; +import { describe, it, expect } from "vitest"; + +import prisma from "@calcom/prisma"; + +import handler from "../../../../pages/api/bookings/[id]/_patch"; + +type CustomNextApiRequest = NextApiRequest & Request; +type CustomNextApiResponse = NextApiResponse & Response; + +describe("PATCH /api/bookings", () => { + it("Returns 403 when user has no permission to the booking", async () => { + const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } }); + const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } }); + const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } }); + + const { req, res } = createMocks({ + method: "PATCH", + body: { + title: booking.title, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + userId: memberUser.id, + }, + query: { + id: booking.id, + }, + }); + + req.userId = memberUser.id; + + await handler(req, res); + expect(res.statusCode).toBe(403); + }); + + it("Allows PATCH when user is system-wide admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } }); + const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } }); + const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } }); + + const { req, res } = createMocks({ + method: "PATCH", + body: { + title: booking.title, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + userId: proUser.id, + }, + query: { + id: booking.id, + }, + }); + + req.userId = adminUser.id; + req.isSystemWideAdmin = true; + + await handler(req, res); + expect(res.statusCode).toBe(200); + }); + + it("Allows PATCH when user is org-wide admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member1-acme@example.com" } }); + const booking = await prisma.booking.findFirstOrThrow({ where: { userId: memberUser.id } }); + + const { req, res } = createMocks({ + method: "PATCH", + body: { + title: booking.title, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + userId: memberUser.id, + }, + query: { + id: booking.id, + }, + }); + + req.userId = adminUser.id; + req.isOrganizationOwnerOrAdmin = true; + + await handler(req, res); + expect(res.statusCode).toBe(200); + }); +}); diff --git a/apps/api/v1/test/lib/bookings/_auth-middleware.integration-test.ts b/apps/api/v1/test/lib/bookings/_auth-middleware.integration-test.ts new file mode 100644 index 0000000000..b0fd7d888e --- /dev/null +++ b/apps/api/v1/test/lib/bookings/_auth-middleware.integration-test.ts @@ -0,0 +1,92 @@ +import type { Request, Response } from "express"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { createMocks } from "node-mocks-http"; +import { describe, it, expect } from "vitest"; + +import type { HttpError } from "@calcom/lib/http-error"; +import prisma from "@calcom/prisma"; + +import authMiddleware from "../../../pages/api/bookings/[id]/_auth-middleware"; + +type CustomNextApiRequest = NextApiRequest & Request; +type CustomNextApiResponse = NextApiResponse & Response; + +describe("Bookings auth middleware", () => { + it("Returns 403 when user has no permission to the booking", async () => { + const trialUser = await prisma.user.findFirstOrThrow({ where: { email: "trial@example.com" } }); + const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } }); + const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } }); + + const { req } = createMocks({ + method: "GET", + body: {}, + query: { + id: booking.id, + }, + }); + + req.userId = trialUser.id; + + try { + await authMiddleware(req); + } catch (error) { + const httpError = error as HttpError; + expect(httpError.statusCode).toBe(403); + } + }); + + it("No error is thrown when user is the booking user", async () => { + const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } }); + const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } }); + + const { req } = createMocks({ + method: "GET", + body: {}, + query: { + id: booking.id, + }, + }); + + req.userId = proUser.id; + + await authMiddleware(req); + }); + + it("No error is thrown when user is system-wide admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } }); + const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } }); + const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } }); + + const { req } = createMocks({ + method: "GET", + body: {}, + query: { + id: booking.id, + }, + }); + + req.userId = adminUser.id; + req.isSystemWideAdmin = true; + + await authMiddleware(req); + }); + + it("No error is thrown when user is org-wide admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member1-acme@example.com" } }); + const booking = await prisma.booking.findFirstOrThrow({ where: { userId: memberUser.id } }); + + const { req } = createMocks({ + method: "GET", + body: {}, + query: { + id: booking.id, + }, + }); + + req.userId = adminUser.id; + req.isOrganizationOwnerOrAdmin = true; + + await authMiddleware(req); + }); +}); diff --git a/apps/api/v1/test/lib/bookings/_get.integration-test.ts b/apps/api/v1/test/lib/bookings/_get.integration-test.ts new file mode 100644 index 0000000000..7687f5e6c8 --- /dev/null +++ b/apps/api/v1/test/lib/bookings/_get.integration-test.ts @@ -0,0 +1,108 @@ +import type { Request, Response } from "express"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { createMocks } from "node-mocks-http"; +import { describe, expect, it } from "vitest"; + +import prisma from "@calcom/prisma"; + +import { handler } from "../../../pages/api/bookings/_get"; + +type CustomNextApiRequest = NextApiRequest & Request; +type CustomNextApiResponse = NextApiResponse & Response; + +const DefaultPagination = { + take: 10, + skip: 0, +}; + +describe("GET /api/bookings", async () => { + const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } }); + const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } }); + + it("Does not return bookings of other users when user has no permission", async () => { + const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } }); + + const { req } = createMocks({ + method: "GET", + query: { + userId: proUser.id, + }, + pagination: DefaultPagination, + }); + + req.userId = memberUser.id; + + const responseData = await handler(req); + const groupedUsers = new Set(responseData.bookings.map((b) => b.userId)); + + expect(responseData.bookings.find((b) => b.userId === memberUser.id)).toBeDefined(); + expect(groupedUsers.size).toBe(1); + expect(groupedUsers.entries().next().value[0]).toBe(memberUser.id); + }); + + it("Returns bookings for regular user", async () => { + const { req } = createMocks({ + method: "GET", + pagination: DefaultPagination, + }); + + req.userId = proUser.id; + + const responseData = await handler(req); + expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); + expect(responseData.bookings.find((b) => b.userId !== proUser.id)).toBeUndefined(); + }); + + it("Returns bookings for specified user when accessed by system-wide admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const { req } = createMocks({ + method: "GET", + pagination: DefaultPagination, + query: { + userId: proUser.id, + }, + }); + + req.isSystemWideAdmin = true; + req.userId = adminUser.id; + + const responseData = await handler(req); + expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); + expect(responseData.bookings.find((b) => b.userId !== proUser.id)).toBeUndefined(); + }); + + it("Returns bookings for all users when accessed by system-wide admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const { req } = createMocks({ + method: "GET", + pagination: { + take: 100, + skip: 0, + }, + }); + + req.isSystemWideAdmin = true; + req.userId = adminUser.id; + + const responseData = await handler(req); + const groupedUsers = new Set(responseData.bookings.map((b) => b.userId)); + expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); + expect(groupedUsers.size).toBeGreaterThan(2); + }); + + it("Returns bookings for org users when accessed by org admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const { req } = createMocks({ + method: "GET", + pagination: DefaultPagination, + }); + + req.userId = adminUser.id; + req.isOrganizationOwnerOrAdmin = true; + + const responseData = await handler(req); + const groupedUsers = new Set(responseData.bookings.map((b) => b.userId)); + expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeUndefined(); + expect(groupedUsers.size).toBeGreaterThanOrEqual(2); + }); +}); diff --git a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts index 84c057c222..836246bbb9 100644 --- a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts +++ b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts @@ -55,7 +55,7 @@ describe("GET /api/event-types/[id]", () => { }) ); - req.isAdmin = true; + req.isSystemWideAdmin = true; req.userId = 333333; await handler(req, res); @@ -91,7 +91,7 @@ describe("GET /api/event-types/[id]", () => { ], }); - req.isAdmin = false; + req.isSystemWideAdmin = false; req.userId = userId; await handler(req, res); @@ -131,7 +131,7 @@ describe("GET /api/event-types/[id]", () => { }) ); - req.isAdmin = false; + req.isSystemWideAdmin = false; req.userId = userId; await handler(req, res); diff --git a/apps/api/v1/test/lib/middleware/verifyApiKey.test.ts b/apps/api/v1/test/lib/middleware/verifyApiKey.test.ts index 764c0daee1..1b53565fd7 100644 --- a/apps/api/v1/test/lib/middleware/verifyApiKey.test.ts +++ b/apps/api/v1/test/lib/middleware/verifyApiKey.test.ts @@ -1,13 +1,17 @@ +import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock"; + import type { Request, Response } from "express"; import type { NextApiRequest, NextApiResponse } from "next"; import { createMocks } from "node-mocks-http"; import { describe, vi, it, expect, afterEach } from "vitest"; import checkLicense from "@calcom/features/ee/common/server/checkLicense"; +import prisma from "@calcom/prisma"; import { isAdminGuard } from "~/lib/utils/isAdmin"; import { verifyApiKey } from "../../../lib/helpers/verifyApiKey"; +import { ScopeOfAdmin } from "../../../lib/utils/scopeOfAdmin"; type CustomNextApiRequest = NextApiRequest & Request; type CustomNextApiResponse = NextApiResponse & Response; @@ -40,7 +44,7 @@ describe("Verify API key", () => { }; vi.mocked(checkLicense).mockResolvedValue(false); - vi.mocked(isAdminGuard).mockResolvedValue(false); + vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null }); const serverNext = vi.fn((next: void) => Promise.resolve(next)); @@ -51,7 +55,7 @@ describe("Verify API key", () => { expect(middlewareSpy).toBeCalled(); expect(res.statusCode).toBe(401); }); - it("It should thow an error if no api key is provided", async () => { + it("It should throw an error if no api key is provided", async () => { const { req, res } = createMocks({ method: "POST", body: {}, @@ -62,7 +66,7 @@ describe("Verify API key", () => { }; vi.mocked(checkLicense).mockResolvedValue(true); - vi.mocked(isAdminGuard).mockResolvedValue(false); + vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null }); const serverNext = vi.fn((next: void) => Promise.resolve(next)); @@ -73,4 +77,70 @@ describe("Verify API key", () => { expect(middlewareSpy).toBeCalled(); expect(res.statusCode).toBe(401); }); + + it("It should set correct permissions for system-wide admin", async () => { + const { req, res } = createMocks({ + method: "POST", + body: {}, + query: { + apiKey: "cal_test_key", + }, + prisma, + }); + + prismaMock.apiKey.findUnique.mockResolvedValue({ + id: 1, + userId: 2, + }); + + const middleware = { + fn: verifyApiKey, + }; + + vi.mocked(checkLicense).mockResolvedValue(true); + vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.SystemWide }); + + const serverNext = vi.fn((next: void) => Promise.resolve(next)); + + const middlewareSpy = vi.spyOn(middleware, "fn"); + + await middleware.fn(req, res, serverNext); + + expect(middlewareSpy).toBeCalled(); + expect(req.isSystemWideAdmin).toBe(true); + expect(req.isOrganizationOwnerOrAdmin).toBe(false); + }); + + it("It should set correct permissions for org-level admin", async () => { + const { req, res } = createMocks({ + method: "POST", + body: {}, + query: { + apiKey: "cal_test_key", + }, + prisma, + }); + + prismaMock.apiKey.findUnique.mockResolvedValue({ + id: 1, + userId: 2, + }); + + const middleware = { + fn: verifyApiKey, + }; + + vi.mocked(checkLicense).mockResolvedValue(true); + vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin }); + + const serverNext = vi.fn((next: void) => Promise.resolve(next)); + + const middlewareSpy = vi.spyOn(middleware, "fn"); + + await middleware.fn(req, res, serverNext); + + expect(middlewareSpy).toBeCalled(); + expect(req.isSystemWideAdmin).toBe(false); + expect(req.isOrganizationOwnerOrAdmin).toBe(true); + }); }); diff --git a/apps/api/v1/test/lib/utils/isAdmin.integration-test.ts b/apps/api/v1/test/lib/utils/isAdmin.integration-test.ts new file mode 100644 index 0000000000..43dca2c9e7 --- /dev/null +++ b/apps/api/v1/test/lib/utils/isAdmin.integration-test.ts @@ -0,0 +1,76 @@ +import type { Request, Response } from "express"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { createMocks } from "node-mocks-http"; +import { describe, it, expect } from "vitest"; + +import prisma from "@calcom/prisma"; + +import { isAdminGuard } from "../../../lib/utils/isAdmin"; +import { ScopeOfAdmin } from "../../../lib/utils/scopeOfAdmin"; + +type CustomNextApiRequest = NextApiRequest & Request; +type CustomNextApiResponse = NextApiResponse & Response; + +describe("isAdmin guard", () => { + it("Returns false when user does not exist in the system", async () => { + const { req } = createMocks({ + method: "POST", + body: {}, + }); + + req.userId = 0; + + const { isAdmin, scope } = await isAdminGuard(req); + + expect(isAdmin).toBe(false); + expect(scope).toBe(null); + }); + + it("Returns false when org user is a member", async () => { + const { req } = createMocks({ + method: "POST", + body: {}, + }); + + const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } }); + + req.userId = memberUser.id; + + const { isAdmin, scope } = await isAdminGuard(req); + + expect(isAdmin).toBe(false); + expect(scope).toBe(null); + }); + + it("Returns system-wide admin when user is marked as such", async () => { + const { req } = createMocks({ + method: "POST", + body: {}, + }); + + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } }); + + req.userId = adminUser.id; + + const { isAdmin, scope } = await isAdminGuard(req); + + expect(isAdmin).toBe(true); + expect(scope).toBe(ScopeOfAdmin.SystemWide); + }); + + it("Returns org-wide admin when user is set as such", async () => { + const { req } = createMocks({ + method: "POST", + body: {}, + }); + + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + + req.userId = adminUser.id; + + const { isAdmin, scope } = await isAdminGuard(req); + + expect(isAdmin).toBe(true); + expect(scope).toBe(ScopeOfAdmin.OrgOwnerOrAdmin); + }); +}); diff --git a/apps/api/v1/test/lib/utils/retrieveScopedAccessibleUsers.integration-test.ts b/apps/api/v1/test/lib/utils/retrieveScopedAccessibleUsers.integration-test.ts new file mode 100644 index 0000000000..2314aaa50d --- /dev/null +++ b/apps/api/v1/test/lib/utils/retrieveScopedAccessibleUsers.integration-test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect } from "vitest"; + +import prisma from "@calcom/prisma"; + +import { + getAccessibleUsers, + retrieveOrgScopedAccessibleUsers, +} from "../../../lib/utils/retrieveScopedAccessibleUsers"; + +describe("retrieveScopedAccessibleUsers tests", () => { + describe("getAccessibleUsers", () => { + it("Does not return members when only admin user ID is supplied", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const accessibleUserIds = await getAccessibleUsers({ + memberUserIds: [], + adminUserId: adminUser.id, + }); + + expect(accessibleUserIds.length).toBe(0); + }); + + it("Does not return members when admin user ID is not an admin of the user", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-dunder@example.com" } }); + const memberOneUser = await prisma.user.findFirstOrThrow({ + where: { email: "member1-acme@example.com" }, + }); + const accessibleUserIds = await getAccessibleUsers({ + memberUserIds: [memberOneUser.id], + adminUserId: adminUser.id, + }); + + expect(accessibleUserIds.length).toBe(0); + }); + + it("Returns members when admin user ID is supplied and members IDs are supplied", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const memberOneUser = await prisma.user.findFirstOrThrow({ + where: { email: "member1-acme@example.com" }, + }); + const memberTwoUser = await prisma.user.findFirstOrThrow({ + where: { email: "member2-acme@example.com" }, + }); + const accessibleUserIds = await getAccessibleUsers({ + memberUserIds: [memberOneUser.id, memberTwoUser.id], + adminUserId: adminUser.id, + }); + + expect(accessibleUserIds.length).toBe(2); + expect(accessibleUserIds).toContain(memberOneUser.id); + expect(accessibleUserIds).toContain(memberTwoUser.id); + }); + }); + + describe("retrieveOrgScopedAccessibleUsers", () => { + it("Does not return members when admin user ID is an admin of an org", async () => { + const memberOneUser = await prisma.user.findFirstOrThrow({ + where: { email: "member1-acme@example.com" }, + }); + + const accessibleUserIds = await retrieveOrgScopedAccessibleUsers({ + adminId: memberOneUser.id, + }); + + expect(accessibleUserIds.length).toBe(0); + }); + + it("Returns members when admin user ID is an admin of an org", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ + where: { email: "owner1-acme@example.com" }, + }); + + const accessibleUserIds = await retrieveOrgScopedAccessibleUsers({ + adminId: adminUser.id, + }); + + const memberOneUser = await prisma.user.findFirstOrThrow({ + where: { email: "member1-acme@example.com" }, + }); + + const memberTwoUser = await prisma.user.findFirstOrThrow({ + where: { email: "member2-acme@example.com" }, + }); + + expect(accessibleUserIds.length).toBe(3); + expect(accessibleUserIds).toContain(memberOneUser.id); + expect(accessibleUserIds).toContain(memberTwoUser.id); + expect(accessibleUserIds).toContain(adminUser.id); + }); + }); +}); diff --git a/packages/prisma/seed.ts b/packages/prisma/seed.ts index 3d8aed80db..874673df9d 100644 --- a/packages/prisma/seed.ts +++ b/packages/prisma/seed.ts @@ -116,21 +116,32 @@ async function createOrganizationAndAddMembersAndTeams({ }; })[] = []; - // Create all users first try { for (const member of orgMembers) { - const orgMemberInDb = { - ...(await prisma.user.create({ - data: { - ...member.memberData, - emailVerified: new Date(), - password: { - create: { - hash: await hashPassword(member.memberData.password.create?.hash || ""), + const newUser = await createUserAndEventType({ + user: { + ...member.memberData, + password: member.memberData.password.create?.hash, + }, + eventTypes: [ + { + title: "30min", + slug: "30min", + length: 30, + _bookings: [ + { + uid: uuid(), + title: "30min", + startTime: dayjs().add(1, "day").toDate(), + endTime: dayjs().add(1, "day").add(30, "minutes").toDate(), }, - }, + ], }, - })), + ], + }); + + const orgMemberInDb = { + ...newUser, inTeams: member.inTeams, orgMembership: member.orgMembership, orgProfile: member.orgProfile, @@ -145,6 +156,7 @@ async function createOrganizationAndAddMembersAndTeams({ return; } } + console.error(e); } await Promise.all([ diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 1ca9f44dd5..71868f1545 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -36,7 +36,24 @@ const workspaces = packagedEmbedTestsOnly }, ] : // It doesn't seem to be possible to fake timezone per test, so we rerun the entire suite with different TZ. See https://github.com/vitest-dev/vitest/issues/1575#issuecomment-1439286286 - timeZoneDependentTestsOnly + integrationTestsOnly + ? [ + { + test: { + name: `IntegrationTests`, + include: ["packages/**/*.integration-test.ts", "apps/**/*.integration-test.ts"], + // TODO: Ignore the api until tests are fixed + exclude: ["**/node_modules/**/*", "packages/embeds/**/*"], + setupFiles: ["setupVitest.ts"], + }, + resolve: { + alias: { + "~": new URL("./apps/api/v1", import.meta.url).pathname, + }, + }, + }, + ] + : timeZoneDependentTestsOnly ? [ { test: {