Skip to content

Commit 01be9f1

Browse files
authored
fix: validate owner email on platform org creation (calcom#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
1 parent 30ef388 commit 01be9f1

4 files changed

Lines changed: 108 additions & 2 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
2+
3+
import { describe, expect, it, vi, beforeEach } from "vitest";
4+
import { UserPermissionRole } from "@calcom/prisma/enums";
5+
import { createHandler } from "./create.handler";
6+
7+
vi.mock("@calcom/lib/constants", async (importOriginal) => {
8+
const actual = await importOriginal<typeof import("@calcom/lib/constants")>();
9+
return {
10+
...actual,
11+
RESERVED_SUBDOMAINS: [],
12+
ORG_SELF_SERVE_ENABLED: true,
13+
ORG_MINIMUM_PUBLISHED_TEAMS_SELF_SERVE: 0,
14+
};
15+
});
16+
17+
vi.mock("@calcom/lib/domainManager/organization", () => ({
18+
createDomain: vi.fn().mockResolvedValue(true),
19+
}));
20+
21+
vi.mock("@calcom/lib/server/i18n", () => ({
22+
getTranslation: vi.fn().mockResolvedValue((key: string) => key),
23+
}));
24+
25+
const createTestUser = async (overrides: { email: string; role?: UserPermissionRole }) => {
26+
return prismock.user.create({
27+
data: {
28+
email: overrides.email,
29+
username: overrides.email.split("@")[0],
30+
role: overrides.role ?? UserPermissionRole.USER,
31+
completedOnboarding: true,
32+
emailVerified: new Date(),
33+
},
34+
});
35+
};
36+
37+
const createInput = (overrides: Partial<Parameters<typeof createHandler>[0]["input"]> = {}) => ({
38+
name: "Test Org",
39+
slug: "test-org",
40+
orgOwnerEmail: "owner@example.com",
41+
isPlatform: false,
42+
creationSource: "WEBAPP" as const,
43+
...overrides,
44+
});
45+
46+
describe("createHandler", () => {
47+
beforeEach(async () => {
48+
vi.clearAllMocks();
49+
await prismock.reset();
50+
});
51+
52+
describe("organization ownership authorization", () => {
53+
it.each([
54+
{ orgOwnerEmail: "other@example.com" },
55+
{ orgOwnerEmail: "other@example.com", isPlatform: true },
56+
])("rejects non-admin creating org for another user (%o)", async (inputOverrides) => {
57+
const user = await createTestUser({ email: "user@example.com" });
58+
59+
await expect(
60+
createHandler({
61+
input: createInput(inputOverrides),
62+
ctx: { user },
63+
})
64+
).rejects.toMatchObject({
65+
code: "FORBIDDEN",
66+
message: "You can only create organization where you are the owner",
67+
});
68+
});
69+
70+
it("allows admin to bypass owner email restriction", async () => {
71+
const admin = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN });
72+
await createTestUser({ email: "owner@example.com" });
73+
74+
const adminWithProfile = {
75+
...admin,
76+
profile: admin.profile ?? { organizationId: null },
77+
};
78+
79+
const result = await createHandler({
80+
input: createInput({ orgOwnerEmail: "owner@example.com" }),
81+
ctx: { user: adminWithProfile },
82+
});
83+
84+
expect(result.email).toBe("owner@example.com");
85+
});
86+
});
87+
});

packages/trpc/server/routers/viewer/organizations/create.handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
9292
throw new TRPCError({ code: "FORBIDDEN", message: "Only admins can create organizations" });
9393
}
9494

95-
if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail && !isPlatform) {
95+
if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail) {
9696
throw new TRPCError({
9797
code: "FORBIDDEN",
9898
message: "You can only create organization where you are the owner",

packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,25 @@ describe("intentToCreateOrgHandler", () => {
281281
);
282282
});
283283

284+
it("should reject non-admin creating org for another user even with isPlatform flag", async () => {
285+
const nonAdminUser = await createTestUser({
286+
email: "nonadmin@example.com",
287+
role: UserPermissionRole.USER,
288+
});
289+
290+
await expect(
291+
intentToCreateOrgHandler({
292+
input: { ...mockInput, isPlatform: true },
293+
ctx: {
294+
user: nonAdminUser,
295+
},
296+
})
297+
).rejects.toMatchObject({
298+
code: "FORBIDDEN",
299+
message: "You can only create organization where you are the owner",
300+
});
301+
});
302+
284303
it("should throw error when target user is not found", async () => {
285304
// Create admin user
286305
const adminUser = await createTestUser({

packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const intentToCreateOrgHandler = async ({ input, ctx }: CreateOptions) =>
5252
const IS_USER_ADMIN = loggedInUser.role === UserPermissionRole.ADMIN;
5353
log.debug("User authorization check", safeStringify({ userId: loggedInUser.id, isAdmin: IS_USER_ADMIN }));
5454

55-
if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail && !isPlatform) {
55+
if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail) {
5656
log.warn(
5757
"Unauthorized organization creation attempt",
5858
safeStringify({ loggedInUserEmail: loggedInUser.email, orgOwnerEmail })

0 commit comments

Comments
 (0)