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
This commit is contained in:
@@ -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<typeof import("@calcom/lib/constants")>();
|
||||
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<Parameters<typeof createHandler>[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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user