Skip to content

Commit e26a57b

Browse files
fix: prisma serialisation errors (calcom#24452)
* Prisma serilaization errors * Rename integration tests * Update packages/features/pbac/infrastructure/repositories/__tests__/PermissionRepository.integration-test.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent b5585b3 commit e26a57b

2 files changed

Lines changed: 344 additions & 26 deletions

File tree

packages/features/pbac/infrastructure/repositories/PermissionRepository.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -132,35 +132,29 @@ export class PermissionRepository implements IPermissionRepository {
132132
const { resource, action } = parsePermissionString(p);
133133
return { resource, action };
134134
});
135-
const resourceActions = permissionPairs.map((p) => [p.resource, p.action]);
136-
const resources = permissionPairs.map((p) => p.resource);
137-
const actions = permissionPairs.map((p) => p.action);
138135

139-
const matchingPermissions = await this.client.$queryRaw<[{ count: bigint }]>`
140-
WITH permission_checks AS (
141-
-- Universal permission (*,*)
142-
SELECT 1 as match FROM "RolePermission"
143-
WHERE "roleId" = ${roleId} AND "resource" = '*' AND "action" = '*'
144-
145-
UNION ALL
146-
147-
-- Wildcard resource with specific actions
148-
SELECT 1 as match FROM "RolePermission"
149-
WHERE "roleId" = ${roleId} AND "resource" = '*' AND "action" = ANY(${actions})
150-
151-
UNION ALL
136+
// Convert permission pairs to JSONB for proper serialization
137+
const permissionPairsJson = JSON.stringify(permissionPairs);
152138

153-
-- Specific resources with wildcard action
154-
SELECT 1 as match FROM "RolePermission"
155-
WHERE "roleId" = ${roleId} AND "action" = '*' AND "resource" = ANY(${resources})
156-
157-
UNION ALL
158-
159-
-- Exact resource-action pairs
160-
SELECT 1 as match FROM "RolePermission"
161-
WHERE "roleId" = ${roleId} AND ("resource", "action") = ANY(${resourceActions})
139+
// Check if each requested permission is satisfied by at least one role permission
140+
const matchingPermissions = await this.client.$queryRaw<[{ count: bigint }]>`
141+
SELECT COUNT(*) as count
142+
FROM jsonb_array_elements(${permissionPairsJson}::jsonb) AS required_perm
143+
WHERE EXISTS (
144+
SELECT 1
145+
FROM "RolePermission" rp
146+
WHERE rp."roleId" = ${roleId}
147+
AND (
148+
-- Universal permission (*,*)
149+
(rp."resource" = '*' AND rp."action" = '*') OR
150+
-- Wildcard resource with specific action
151+
(rp."resource" = '*' AND rp."action" = required_perm->>'action') OR
152+
-- Specific resource with wildcard action
153+
(rp."resource" = required_perm->>'resource' AND rp."action" = '*') OR
154+
-- Exact resource-action pair
155+
(rp."resource" = required_perm->>'resource' AND rp."action" = required_perm->>'action')
156+
)
162157
)
163-
SELECT COUNT(*) as count FROM permission_checks
164158
`;
165159

166160
return Number(matchingPermissions[0].count) >= permissions.length;
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
2+
3+
import { prisma } from "@calcom/prisma";
4+
5+
import type { PermissionString } from "../../../domain/types/permission-registry";
6+
import { PermissionRepository } from "../PermissionRepository";
7+
8+
describe("PermissionRepository - Integration Tests", () => {
9+
let repository: PermissionRepository;
10+
let testRoleId: string;
11+
let testUserId: number;
12+
let testTeamId: number;
13+
14+
beforeAll(async () => {
15+
repository = new PermissionRepository(prisma);
16+
});
17+
18+
beforeEach(async () => {
19+
// Create test user
20+
const testUser = await prisma.user.create({
21+
data: {
22+
email: `test-${Date.now()}@example.com`,
23+
username: `testuser-${Date.now()}`,
24+
},
25+
});
26+
testUserId = testUser.id;
27+
28+
// Create test team
29+
const testTeam = await prisma.team.create({
30+
data: {
31+
name: `Test Team ${Date.now()}`,
32+
slug: `test-team-${Date.now()}`,
33+
},
34+
});
35+
testTeamId = testTeam.id;
36+
37+
// Create test role
38+
const testRole = await prisma.role.create({
39+
data: {
40+
name: `Test Role ${Date.now()}`,
41+
teamId: testTeamId,
42+
},
43+
});
44+
testRoleId = testRole.id;
45+
});
46+
47+
afterEach(async () => {
48+
// Clean up test data
49+
await prisma.rolePermission.deleteMany({
50+
where: { roleId: testRoleId },
51+
});
52+
await prisma.membership.deleteMany({
53+
where: { userId: testUserId },
54+
});
55+
await prisma.role.deleteMany({
56+
where: { id: testRoleId },
57+
});
58+
await prisma.team.deleteMany({
59+
where: { id: testTeamId },
60+
});
61+
await prisma.user.deleteMany({
62+
where: { id: testUserId },
63+
});
64+
});
65+
66+
67+
describe("checkRolePermissions", () => {
68+
it("should successfully check single permission without serialization error", async () => {
69+
// Create a role permission
70+
await prisma.rolePermission.create({
71+
data: {
72+
roleId: testRoleId,
73+
resource: "role",
74+
action: "create",
75+
},
76+
});
77+
78+
const permissions: PermissionString[] = ["role.create"];
79+
const result = await repository.checkRolePermissions(testRoleId, permissions);
80+
81+
expect(result).toBe(true);
82+
});
83+
84+
it("should successfully check multiple permissions without serialization error", async () => {
85+
// Create multiple role permissions
86+
await prisma.rolePermission.createMany({
87+
data: [
88+
{ roleId: testRoleId, resource: "role", action: "create" },
89+
{ roleId: testRoleId, resource: "role", action: "read" },
90+
{ roleId: testRoleId, resource: "eventType", action: "update" },
91+
],
92+
});
93+
94+
const permissions: PermissionString[] = ["role.create", "role.read", "eventType.update"];
95+
const result = await repository.checkRolePermissions(testRoleId, permissions);
96+
97+
expect(result).toBe(true);
98+
});
99+
100+
it("should handle exact resource-action pair matching", async () => {
101+
await prisma.rolePermission.create({
102+
data: {
103+
roleId: testRoleId,
104+
resource: "team",
105+
action: "invite",
106+
},
107+
});
108+
109+
const result = await repository.checkRolePermissions(testRoleId, ["team.invite"]);
110+
111+
expect(result).toBe(true);
112+
});
113+
114+
it("should handle wildcard resource (*) with specific action", async () => {
115+
await prisma.rolePermission.create({
116+
data: {
117+
roleId: testRoleId,
118+
resource: "*",
119+
action: "read",
120+
},
121+
});
122+
123+
// Should match any resource with read action
124+
const result = await repository.checkRolePermissions(testRoleId, [
125+
"eventType.read",
126+
"team.read",
127+
"role.read",
128+
]);
129+
130+
expect(result).toBe(true);
131+
});
132+
133+
it("should handle specific resource with wildcard action (*)", async () => {
134+
await prisma.rolePermission.create({
135+
data: {
136+
roleId: testRoleId,
137+
resource: "eventType",
138+
action: "*",
139+
},
140+
});
141+
142+
// Should match eventType with any action
143+
const result = await repository.checkRolePermissions(testRoleId, [
144+
"eventType.create",
145+
"eventType.read",
146+
"eventType.update",
147+
"eventType.delete",
148+
]);
149+
150+
expect(result).toBe(true);
151+
});
152+
153+
it("should handle universal wildcard (*.*)", async () => {
154+
await prisma.rolePermission.create({
155+
data: {
156+
roleId: testRoleId,
157+
resource: "*",
158+
action: "*",
159+
},
160+
});
161+
162+
// Should match any permission
163+
const result = await repository.checkRolePermissions(testRoleId, [
164+
"eventType.create",
165+
"team.delete",
166+
"role.update",
167+
]);
168+
169+
expect(result).toBe(true);
170+
});
171+
172+
it("should return false when not all permissions are matched", async () => {
173+
await prisma.rolePermission.createMany({
174+
data: [
175+
{ roleId: testRoleId, resource: "role", action: "create" },
176+
{ roleId: testRoleId, resource: "role", action: "read" },
177+
],
178+
});
179+
180+
// Missing eventType.update permission
181+
const result = await repository.checkRolePermissions(testRoleId, [
182+
"role.create",
183+
"role.read",
184+
"eventType.update",
185+
]);
186+
187+
expect(result).toBe(false);
188+
});
189+
190+
it("should return false for empty permissions array", async () => {
191+
const result = await repository.checkRolePermissions(testRoleId, []);
192+
193+
expect(result).toBe(false);
194+
});
195+
196+
it("should handle complex permission combinations", async () => {
197+
await prisma.rolePermission.createMany({
198+
data: [
199+
{ roleId: testRoleId, resource: "*", action: "read" }, // wildcard resource
200+
{ roleId: testRoleId, resource: "eventType", action: "*" }, // wildcard action
201+
{ roleId: testRoleId, resource: "team", action: "invite" }, // exact match
202+
],
203+
});
204+
205+
// All should match:
206+
// - role.read matches wildcard resource
207+
// - eventType.create matches wildcard action for eventType
208+
// - team.invite matches exact pair
209+
const result = await repository.checkRolePermissions(testRoleId, [
210+
"role.read",
211+
"eventType.create",
212+
"team.invite",
213+
]);
214+
215+
expect(result).toBe(true);
216+
});
217+
218+
it("should handle permission pairs with same resource but different actions", async () => {
219+
await prisma.rolePermission.createMany({
220+
data: [
221+
{ roleId: testRoleId, resource: "eventType", action: "create" },
222+
{ roleId: testRoleId, resource: "eventType", action: "update" },
223+
{ roleId: testRoleId, resource: "eventType", action: "delete" },
224+
],
225+
});
226+
227+
const result = await repository.checkRolePermissions(testRoleId, [
228+
"eventType.create",
229+
"eventType.update",
230+
"eventType.delete",
231+
]);
232+
233+
expect(result).toBe(true);
234+
});
235+
236+
it("should handle permission pairs with same action but different resources", async () => {
237+
await prisma.rolePermission.createMany({
238+
data: [
239+
{ roleId: testRoleId, resource: "eventType", action: "create" },
240+
{ roleId: testRoleId, resource: "team", action: "create" },
241+
{ roleId: testRoleId, resource: "role", action: "create" },
242+
],
243+
});
244+
245+
const result = await repository.checkRolePermissions(testRoleId, [
246+
"eventType.create",
247+
"team.create",
248+
"role.create",
249+
]);
250+
251+
expect(result).toBe(true);
252+
});
253+
254+
it("should correctly count matching permissions", async () => {
255+
await prisma.rolePermission.createMany({
256+
data: [
257+
{ roleId: testRoleId, resource: "eventType", action: "create" },
258+
{ roleId: testRoleId, resource: "eventType", action: "read" },
259+
],
260+
});
261+
262+
// Only 2 out of 3 permissions match
263+
const result = await repository.checkRolePermissions(testRoleId, [
264+
"eventType.create",
265+
"eventType.read",
266+
"eventType.update", // This one doesn't match
267+
]);
268+
269+
expect(result).toBe(false);
270+
});
271+
272+
it("should handle role with no permissions", async () => {
273+
const result = await repository.checkRolePermissions(testRoleId, ["eventType.create"]);
274+
275+
expect(result).toBe(false);
276+
});
277+
278+
it("should handle non-existent role", async () => {
279+
const result = await repository.checkRolePermissions("non-existent-role-id", ["eventType.create"]);
280+
281+
expect(result).toBe(false);
282+
});
283+
284+
it("should reproduce the exact error scenario from the bug report", async () => {
285+
// Create the exact permission from the error report
286+
await prisma.rolePermission.create({
287+
data: {
288+
roleId: testRoleId,
289+
resource: "role",
290+
action: "create",
291+
},
292+
});
293+
294+
// This is the exact call that was failing
295+
const permissions: PermissionString[] = ["role.create"];
296+
const result = await repository.checkRolePermissions(testRoleId, permissions);
297+
298+
// Should not throw serialization error and should return true
299+
expect(result).toBe(true);
300+
});
301+
});
302+
303+
describe("checkRolePermission (single)", () => {
304+
it("should check single permission correctly", async () => {
305+
await prisma.rolePermission.create({
306+
data: {
307+
roleId: testRoleId,
308+
resource: "role",
309+
action: "create",
310+
},
311+
});
312+
313+
const result = await repository.checkRolePermission(testRoleId, "role.create");
314+
315+
expect(result).toBe(true);
316+
});
317+
318+
it("should return false for non-existent permission", async () => {
319+
const result = await repository.checkRolePermission(testRoleId, "role.create");
320+
321+
expect(result).toBe(false);
322+
});
323+
});
324+
});

0 commit comments

Comments
 (0)