From 01be9f1ef230fb1b73899986e20537ddcdd4a0d3 Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Tue, 6 Jan 2026 22:46:44 -0300 Subject: [PATCH] fix: validate owner email on platform org creation (#26286) Remove isPlatform bypass from owner verification to ensure users can only create organizations where they are the designated owner. Add test coverage for create and intentToCreateOrg handlers: - Regression tests for isPlatform bypass fix - Happy path for admin creating org for another user --- .../organizations/create.handler.test.ts | 87 +++++++++++++++++++ .../viewer/organizations/create.handler.ts | 2 +- .../intentToCreateOrg.handler.test.ts | 19 ++++ .../intentToCreateOrg.handler.ts | 2 +- 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 packages/trpc/server/routers/viewer/organizations/create.handler.test.ts diff --git a/packages/trpc/server/routers/viewer/organizations/create.handler.test.ts b/packages/trpc/server/routers/viewer/organizations/create.handler.test.ts new file mode 100644 index 0000000000..4b3f30c9ba --- /dev/null +++ b/packages/trpc/server/routers/viewer/organizations/create.handler.test.ts @@ -0,0 +1,87 @@ +import prismock from "../../../../../../tests/libs/__mocks__/prisma"; + +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { UserPermissionRole } from "@calcom/prisma/enums"; +import { createHandler } from "./create.handler"; + +vi.mock("@calcom/lib/constants", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + RESERVED_SUBDOMAINS: [], + ORG_SELF_SERVE_ENABLED: true, + ORG_MINIMUM_PUBLISHED_TEAMS_SELF_SERVE: 0, + }; +}); + +vi.mock("@calcom/lib/domainManager/organization", () => ({ + createDomain: vi.fn().mockResolvedValue(true), +})); + +vi.mock("@calcom/lib/server/i18n", () => ({ + getTranslation: vi.fn().mockResolvedValue((key: string) => key), +})); + +const createTestUser = async (overrides: { email: string; role?: UserPermissionRole }) => { + return prismock.user.create({ + data: { + email: overrides.email, + username: overrides.email.split("@")[0], + role: overrides.role ?? UserPermissionRole.USER, + completedOnboarding: true, + emailVerified: new Date(), + }, + }); +}; + +const createInput = (overrides: Partial[0]["input"]> = {}) => ({ + name: "Test Org", + slug: "test-org", + orgOwnerEmail: "owner@example.com", + isPlatform: false, + creationSource: "WEBAPP" as const, + ...overrides, +}); + +describe("createHandler", () => { + beforeEach(async () => { + vi.clearAllMocks(); + await prismock.reset(); + }); + + describe("organization ownership authorization", () => { + it.each([ + { orgOwnerEmail: "other@example.com" }, + { orgOwnerEmail: "other@example.com", isPlatform: true }, + ])("rejects non-admin creating org for another user (%o)", async (inputOverrides) => { + const user = await createTestUser({ email: "user@example.com" }); + + await expect( + createHandler({ + input: createInput(inputOverrides), + ctx: { user }, + }) + ).rejects.toMatchObject({ + code: "FORBIDDEN", + message: "You can only create organization where you are the owner", + }); + }); + + it("allows admin to bypass owner email restriction", async () => { + const admin = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN }); + await createTestUser({ email: "owner@example.com" }); + + const adminWithProfile = { + ...admin, + profile: admin.profile ?? { organizationId: null }, + }; + + const result = await createHandler({ + input: createInput({ orgOwnerEmail: "owner@example.com" }), + ctx: { user: adminWithProfile }, + }); + + expect(result.email).toBe("owner@example.com"); + }); + }); +}); diff --git a/packages/trpc/server/routers/viewer/organizations/create.handler.ts b/packages/trpc/server/routers/viewer/organizations/create.handler.ts index 2db25041a6..910fead380 100644 --- a/packages/trpc/server/routers/viewer/organizations/create.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/create.handler.ts @@ -92,7 +92,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => { throw new TRPCError({ code: "FORBIDDEN", message: "Only admins can create organizations" }); } - if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail && !isPlatform) { + if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail) { throw new TRPCError({ code: "FORBIDDEN", message: "You can only create organization where you are the owner", diff --git a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts index 702af45ffd..c707c6783b 100644 --- a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts +++ b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts @@ -281,6 +281,25 @@ describe("intentToCreateOrgHandler", () => { ); }); + it("should reject non-admin creating org for another user even with isPlatform flag", async () => { + const nonAdminUser = await createTestUser({ + email: "nonadmin@example.com", + role: UserPermissionRole.USER, + }); + + await expect( + intentToCreateOrgHandler({ + input: { ...mockInput, isPlatform: true }, + ctx: { + user: nonAdminUser, + }, + }) + ).rejects.toMatchObject({ + code: "FORBIDDEN", + message: "You can only create organization where you are the owner", + }); + }); + it("should throw error when target user is not found", async () => { // Create admin user const adminUser = await createTestUser({ diff --git a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts index 73a502ce79..cf7cfcb850 100644 --- a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts @@ -52,7 +52,7 @@ export const intentToCreateOrgHandler = async ({ input, ctx }: CreateOptions) => const IS_USER_ADMIN = loggedInUser.role === UserPermissionRole.ADMIN; log.debug("User authorization check", safeStringify({ userId: loggedInUser.id, isAdmin: IS_USER_ADMIN })); - if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail && !isPlatform) { + if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail) { log.warn( "Unauthorized organization creation attempt", safeStringify({ loggedInUserEmail: loggedInUser.email, orgOwnerEmail })