feat: Add Organization Admin scope to API V1 (#14860)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Syed Ali Shahbaz <syedshahbaz@Syeds-MacBook-Pro.local> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
co-authored by
Carina Wollendorfer
Joe Au-Yeung
Keith Williams
Syed Ali Shahbaz
zomars
parent
23cbd838c2
commit
bd1664ad6e
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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<number[]>((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<number[]> => {
|
||||
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);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const ScopeOfAdmin = {
|
||||
SystemWide: "SystemWide",
|
||||
OrgOwnerOrAdmin: "OrgOwnerOrAdmin",
|
||||
} as const;
|
||||
Vendored
+2
-1
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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<CustomNextApiReque
|
||||
}
|
||||
|
||||
async function getHandler(req: CustomNextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
req.args = isAdmin ? {} : { where: { userId } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
req.args = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
// Proof of concept: allowing mutation in exchange of composability
|
||||
handleAdminRequests(req);
|
||||
const data = await prisma.apiKey.findMany(req.args);
|
||||
|
||||
@@ -10,7 +10,7 @@ import prisma from "@calcom/prisma";
|
||||
import { apiKeyCreateBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body);
|
||||
const [hashedKey, apiKey] = generateUniqueAPIKey();
|
||||
const args: Prisma.ApiKeyCreateArgs = {
|
||||
@@ -24,9 +24,10 @@ async function postHandler(req: NextApiRequest) {
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -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 query = schemaQueryIdParseInt.parse(req.query);
|
||||
// @note: Here we make sure to only return attendee's of the user's own bookings if the user is not an admin.
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all user bookings, including attendees
|
||||
const attendee = await prisma.attendee.findFirst({
|
||||
where: { id: query.id, booking: { userId } },
|
||||
|
||||
@@ -63,8 +63,8 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaAttendeeEditBodyParams>) {
|
||||
const { isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const { userId } = req;
|
||||
const { bookingId } = body;
|
||||
if (bookingId) {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)) };
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof schemaBookingEditBodyParams>) {
|
||||
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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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)) };
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -87,7 +87,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
req: NextApiRequest,
|
||||
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
|
||||
) {
|
||||
if (req.isAdmin) return true;
|
||||
if (req.isSystemWideAdmin) return true;
|
||||
if (eventType?.teamId) {
|
||||
req.query.teamId = String(eventType.teamId);
|
||||
await canAccessTeamEventOrThrow(req, {
|
||||
|
||||
@@ -237,9 +237,9 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
|
||||
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" });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ export default async function checkTeamEventEditPermission(
|
||||
req: NextApiRequest,
|
||||
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -91,8 +91,8 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaSingleScheduleBodyParams>) {
|
||||
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" });
|
||||
}
|
||||
|
||||
@@ -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 } };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 } };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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`",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 } } } } }] },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 } }] };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
id: booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = adminUser.id;
|
||||
req.isOrganizationOwnerOrAdmin = true;
|
||||
|
||||
await authMiddleware(req);
|
||||
});
|
||||
});
|
||||
@@ -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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+23
-11
@@ -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([
|
||||
|
||||
+18
-1
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user