diff --git a/packages/features/ee/impersonation/lib/ImpersonationProvider.test.ts b/packages/features/ee/impersonation/lib/ImpersonationProvider.test.ts new file mode 100644 index 0000000000..66d517e648 --- /dev/null +++ b/packages/features/ee/impersonation/lib/ImpersonationProvider.test.ts @@ -0,0 +1,81 @@ +import type { Session } from "next-auth"; +import { describe, expect, it } from "vitest"; + +import { UserPermissionRole } from "@calcom/prisma/enums"; + +import { + parseTeamId, + checkSelfImpersonation, + checkUserIdentifier, + checkPermission, +} from "./ImpersonationProvider"; + +const session: Session = { + expires: "2021-08-31T15:00:00.000Z", + hasValidLicense: true, + user: { + id: 123, + username: "test", + role: UserPermissionRole.USER, + email: "test@example.com", + }, +}; + +describe("parseTeamId", () => { + it("should return undefined if no teamId is provided", () => { + expect(parseTeamId(undefined)).toBeUndefined(); + }); + + it("should return the parsed teamId if a teamId is provided", () => { + expect(parseTeamId({ username: "test", teamId: "123" })).toBe(123); + }); + + it("should throw an error if the provided teamId is not a positive number", () => { + expect(() => parseTeamId({ username: "test", teamId: "-123" })).toThrow(); + }); + it("should throw an error if the provided teamId is not a number", () => { + expect(() => parseTeamId({ username: "test", teamId: "notanumber" })).toThrow(); + }); +}); + +describe("checkSelfImpersonation", () => { + it("should throw an error if the provided username is the same as the session user's username", () => { + expect(() => checkSelfImpersonation(session, { username: "test" })).toThrow(); + }); + + it("should throw an error if the provided username is the same as the session user's email", () => { + expect(() => checkSelfImpersonation(session, { username: "test@example.com" })).toThrow(); + }); + + it("should not throw an error if the provided username is different from the session user's username and email", () => { + expect(() => checkSelfImpersonation(session, { username: "other" })).not.toThrow(); + }); +}); + +describe("checkUserIdentifier", () => { + it("should throw an error if no username is provided", () => { + expect(() => checkUserIdentifier(undefined)).toThrow(); + }); + + it("should not throw an error if a username is provided", () => { + expect(() => checkUserIdentifier({ username: "test" })).not.toThrow(); + }); +}); + +describe("checkPermission", () => { + it("should throw an error if the user is not an admin and team impersonation is disabled", () => { + process.env.NEXT_PUBLIC_TEAM_IMPERSONATION = "false"; + expect(() => checkPermission(session)).toThrow(); + }); + + it("should not throw an error if the user is an admin and team impersonation is disabled", () => { + const modifiedSession = { ...session, user: { ...session.user, role: UserPermissionRole.ADMIN } }; + process.env.NEXT_PUBLIC_TEAM_IMPERSONATION = "false"; + expect(() => checkPermission(modifiedSession)).not.toThrow(); + }); + + it("should not throw an error if the user is not an admin but team impersonation is enabled", () => { + process.env.NEXT_PUBLIC_TEAM_IMPERSONATION = "true"; + expect(() => checkPermission(session)).not.toThrow(); + }); +}); diff --git a/packages/features/ee/impersonation/lib/ImpersonationProvider.ts b/packages/features/ee/impersonation/lib/ImpersonationProvider.ts index 9b90bb78d7..23545b0792 100644 --- a/packages/features/ee/impersonation/lib/ImpersonationProvider.ts +++ b/packages/features/ee/impersonation/lib/ImpersonationProvider.ts @@ -1,4 +1,5 @@ import type { User } from "@prisma/client"; +import type { Session } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { z } from "zod"; @@ -44,6 +45,28 @@ const auditAndReturnNextUser = async ( return obj; }; +type Credentials = Record<"username" | "teamId", string> | undefined; + +export function parseTeamId(creds: Partial) { + return creds?.teamId ? teamIdschema.parse({ teamId: creds.teamId }).teamId : undefined; +} + +export function checkSelfImpersonation(session: Session | null, creds: Partial) { + if (session?.user.username === creds?.username || session?.user.email === creds?.username) { + throw new Error("You cannot impersonate yourself."); + } +} + +export function checkUserIdentifier(creds: Partial) { + if (!creds?.username) throw new Error("User identifier must be present"); +} + +export function checkPermission(session: Session | null) { + if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") { + throw new Error("You do not have permission to do this."); + } +} + const ImpersonationProvider = CredentialsProvider({ id: "impersonation-auth", name: "Impersonation", @@ -56,18 +79,10 @@ const ImpersonationProvider = CredentialsProvider({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore need to figure out how to correctly type this const session = await getSession({ req }); - // If teamId is present -> parse the teamId and throw error itn ot number. If not present teamId is set to undefined - const teamId = creds?.teamId ? teamIdschema.parse({ teamId: creds.teamId }).teamId : undefined; - - if (session?.user.username === creds?.username || session?.user.email === creds?.username) { - throw new Error("You cannot impersonate yourself."); - } - - if (!creds?.username) throw new Error("User identifier must be present"); - // If you are an ADMIN we return way before team impersonation logic is executed, so NEXT_PUBLIC_TEAM_IMPERSONATION certainly true - if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") { - throw new Error("You do not have permission to do this."); - } + const teamId = parseTeamId(creds); + checkSelfImpersonation(session, creds); + checkUserIdentifier(creds); + checkPermission(session); // Get user who is being impersonated const impersonatedUser = await prisma.user.findFirst({