fix: prisma serialisation errors (#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>
This commit is contained in:
sean-brydon
2025-10-14 15:46:20 -03:00
committed by GitHub
co-authored by coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent b5585b3974
commit e26a57bf25
2 changed files with 344 additions and 26 deletions
@@ -132,35 +132,29 @@ export class PermissionRepository implements IPermissionRepository {
const { resource, action } = parsePermissionString(p);
return { resource, action };
});
const resourceActions = permissionPairs.map((p) => [p.resource, p.action]);
const resources = permissionPairs.map((p) => p.resource);
const actions = permissionPairs.map((p) => p.action);
// Convert permission pairs to JSONB for proper serialization
const permissionPairsJson = JSON.stringify(permissionPairs);
// Check if each requested permission is satisfied by at least one role permission
const matchingPermissions = await this.client.$queryRaw<[{ count: bigint }]>`
WITH permission_checks AS (
-- Universal permission (*,*)
SELECT 1 as match FROM "RolePermission"
WHERE "roleId" = ${roleId} AND "resource" = '*' AND "action" = '*'
UNION ALL
-- Wildcard resource with specific actions
SELECT 1 as match FROM "RolePermission"
WHERE "roleId" = ${roleId} AND "resource" = '*' AND "action" = ANY(${actions})
UNION ALL
-- Specific resources with wildcard action
SELECT 1 as match FROM "RolePermission"
WHERE "roleId" = ${roleId} AND "action" = '*' AND "resource" = ANY(${resources})
UNION ALL
-- Exact resource-action pairs
SELECT 1 as match FROM "RolePermission"
WHERE "roleId" = ${roleId} AND ("resource", "action") = ANY(${resourceActions})
SELECT COUNT(*) as count
FROM jsonb_array_elements(${permissionPairsJson}::jsonb) AS required_perm
WHERE EXISTS (
SELECT 1
FROM "RolePermission" rp
WHERE rp."roleId" = ${roleId}
AND (
-- Universal permission (*,*)
(rp."resource" = '*' AND rp."action" = '*') OR
-- Wildcard resource with specific action
(rp."resource" = '*' AND rp."action" = required_perm->>'action') OR
-- Specific resource with wildcard action
(rp."resource" = required_perm->>'resource' AND rp."action" = '*') OR
-- Exact resource-action pair
(rp."resource" = required_perm->>'resource' AND rp."action" = required_perm->>'action')
)
)
SELECT COUNT(*) as count FROM permission_checks
`;
return Number(matchingPermissions[0].count) >= permissions.length;
@@ -0,0 +1,324 @@
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
import { prisma } from "@calcom/prisma";
import type { PermissionString } from "../../../domain/types/permission-registry";
import { PermissionRepository } from "../PermissionRepository";
describe("PermissionRepository - Integration Tests", () => {
let repository: PermissionRepository;
let testRoleId: string;
let testUserId: number;
let testTeamId: number;
beforeAll(async () => {
repository = new PermissionRepository(prisma);
});
beforeEach(async () => {
// Create test user
const testUser = await prisma.user.create({
data: {
email: `test-${Date.now()}@example.com`,
username: `testuser-${Date.now()}`,
},
});
testUserId = testUser.id;
// Create test team
const testTeam = await prisma.team.create({
data: {
name: `Test Team ${Date.now()}`,
slug: `test-team-${Date.now()}`,
},
});
testTeamId = testTeam.id;
// Create test role
const testRole = await prisma.role.create({
data: {
name: `Test Role ${Date.now()}`,
teamId: testTeamId,
},
});
testRoleId = testRole.id;
});
afterEach(async () => {
// Clean up test data
await prisma.rolePermission.deleteMany({
where: { roleId: testRoleId },
});
await prisma.membership.deleteMany({
where: { userId: testUserId },
});
await prisma.role.deleteMany({
where: { id: testRoleId },
});
await prisma.team.deleteMany({
where: { id: testTeamId },
});
await prisma.user.deleteMany({
where: { id: testUserId },
});
});
describe("checkRolePermissions", () => {
it("should successfully check single permission without serialization error", async () => {
// Create a role permission
await prisma.rolePermission.create({
data: {
roleId: testRoleId,
resource: "role",
action: "create",
},
});
const permissions: PermissionString[] = ["role.create"];
const result = await repository.checkRolePermissions(testRoleId, permissions);
expect(result).toBe(true);
});
it("should successfully check multiple permissions without serialization error", async () => {
// Create multiple role permissions
await prisma.rolePermission.createMany({
data: [
{ roleId: testRoleId, resource: "role", action: "create" },
{ roleId: testRoleId, resource: "role", action: "read" },
{ roleId: testRoleId, resource: "eventType", action: "update" },
],
});
const permissions: PermissionString[] = ["role.create", "role.read", "eventType.update"];
const result = await repository.checkRolePermissions(testRoleId, permissions);
expect(result).toBe(true);
});
it("should handle exact resource-action pair matching", async () => {
await prisma.rolePermission.create({
data: {
roleId: testRoleId,
resource: "team",
action: "invite",
},
});
const result = await repository.checkRolePermissions(testRoleId, ["team.invite"]);
expect(result).toBe(true);
});
it("should handle wildcard resource (*) with specific action", async () => {
await prisma.rolePermission.create({
data: {
roleId: testRoleId,
resource: "*",
action: "read",
},
});
// Should match any resource with read action
const result = await repository.checkRolePermissions(testRoleId, [
"eventType.read",
"team.read",
"role.read",
]);
expect(result).toBe(true);
});
it("should handle specific resource with wildcard action (*)", async () => {
await prisma.rolePermission.create({
data: {
roleId: testRoleId,
resource: "eventType",
action: "*",
},
});
// Should match eventType with any action
const result = await repository.checkRolePermissions(testRoleId, [
"eventType.create",
"eventType.read",
"eventType.update",
"eventType.delete",
]);
expect(result).toBe(true);
});
it("should handle universal wildcard (*.*)", async () => {
await prisma.rolePermission.create({
data: {
roleId: testRoleId,
resource: "*",
action: "*",
},
});
// Should match any permission
const result = await repository.checkRolePermissions(testRoleId, [
"eventType.create",
"team.delete",
"role.update",
]);
expect(result).toBe(true);
});
it("should return false when not all permissions are matched", async () => {
await prisma.rolePermission.createMany({
data: [
{ roleId: testRoleId, resource: "role", action: "create" },
{ roleId: testRoleId, resource: "role", action: "read" },
],
});
// Missing eventType.update permission
const result = await repository.checkRolePermissions(testRoleId, [
"role.create",
"role.read",
"eventType.update",
]);
expect(result).toBe(false);
});
it("should return false for empty permissions array", async () => {
const result = await repository.checkRolePermissions(testRoleId, []);
expect(result).toBe(false);
});
it("should handle complex permission combinations", async () => {
await prisma.rolePermission.createMany({
data: [
{ roleId: testRoleId, resource: "*", action: "read" }, // wildcard resource
{ roleId: testRoleId, resource: "eventType", action: "*" }, // wildcard action
{ roleId: testRoleId, resource: "team", action: "invite" }, // exact match
],
});
// All should match:
// - role.read matches wildcard resource
// - eventType.create matches wildcard action for eventType
// - team.invite matches exact pair
const result = await repository.checkRolePermissions(testRoleId, [
"role.read",
"eventType.create",
"team.invite",
]);
expect(result).toBe(true);
});
it("should handle permission pairs with same resource but different actions", async () => {
await prisma.rolePermission.createMany({
data: [
{ roleId: testRoleId, resource: "eventType", action: "create" },
{ roleId: testRoleId, resource: "eventType", action: "update" },
{ roleId: testRoleId, resource: "eventType", action: "delete" },
],
});
const result = await repository.checkRolePermissions(testRoleId, [
"eventType.create",
"eventType.update",
"eventType.delete",
]);
expect(result).toBe(true);
});
it("should handle permission pairs with same action but different resources", async () => {
await prisma.rolePermission.createMany({
data: [
{ roleId: testRoleId, resource: "eventType", action: "create" },
{ roleId: testRoleId, resource: "team", action: "create" },
{ roleId: testRoleId, resource: "role", action: "create" },
],
});
const result = await repository.checkRolePermissions(testRoleId, [
"eventType.create",
"team.create",
"role.create",
]);
expect(result).toBe(true);
});
it("should correctly count matching permissions", async () => {
await prisma.rolePermission.createMany({
data: [
{ roleId: testRoleId, resource: "eventType", action: "create" },
{ roleId: testRoleId, resource: "eventType", action: "read" },
],
});
// Only 2 out of 3 permissions match
const result = await repository.checkRolePermissions(testRoleId, [
"eventType.create",
"eventType.read",
"eventType.update", // This one doesn't match
]);
expect(result).toBe(false);
});
it("should handle role with no permissions", async () => {
const result = await repository.checkRolePermissions(testRoleId, ["eventType.create"]);
expect(result).toBe(false);
});
it("should handle non-existent role", async () => {
const result = await repository.checkRolePermissions("non-existent-role-id", ["eventType.create"]);
expect(result).toBe(false);
});
it("should reproduce the exact error scenario from the bug report", async () => {
// Create the exact permission from the error report
await prisma.rolePermission.create({
data: {
roleId: testRoleId,
resource: "role",
action: "create",
},
});
// This is the exact call that was failing
const permissions: PermissionString[] = ["role.create"];
const result = await repository.checkRolePermissions(testRoleId, permissions);
// Should not throw serialization error and should return true
expect(result).toBe(true);
});
});
describe("checkRolePermission (single)", () => {
it("should check single permission correctly", async () => {
await prisma.rolePermission.create({
data: {
roleId: testRoleId,
resource: "role",
action: "create",
},
});
const result = await repository.checkRolePermission(testRoleId, "role.create");
expect(result).toBe(true);
});
it("should return false for non-existent permission", async () => {
const result = await repository.checkRolePermission(testRoleId, "role.create");
expect(result).toBe(false);
});
});
});