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:
Syed Ali Shahbaz
2024-05-21 20:09:20 +00:00
committed by GitHub
co-authored by Carina Wollendorfer Joe Au-Yeung Keith Williams Syed Ali Shahbaz zomars
parent 23cbd838c2
commit bd1664ad6e
76 changed files with 1019 additions and 202 deletions
+6 -2
View File
@@ -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);
+30 -3
View File
@@ -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);
};
+4
View File
@@ -0,0 +1,4 @@
export const ScopeOfAdmin = {
SystemWide: "SystemWide",
OrgOwnerOrAdmin: "OrgOwnerOrAdmin",
} as const;
+2 -1
View File
@@ -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 },
+4 -4
View File
@@ -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);
+4 -3
View File
@@ -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) {
+2 -2
View File
@@ -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" });
+2 -2
View File
@@ -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 },
+2 -2
View File
@@ -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" });
}
}
}
+16 -2
View File
@@ -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);
+92 -16
View File
@@ -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));
+13 -2
View File
@@ -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 } },
+4 -2
View File
@@ -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)) };
}
+2 -2
View File
@@ -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" });
+4 -4
View File
@@ -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);
+7 -7
View File
@@ -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) {
+3 -3
View File
@@ -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({
+9 -7
View File
@@ -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;
+3 -3
View File
@@ -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" });
}
+4 -4
View File
@@ -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 } };
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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) };
}
+4 -4
View File
@@ -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 },
+3 -2
View File
@@ -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;
}
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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;
+5 -4
View File
@@ -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 } }] };
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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: {