feat: add method for getting teamId permissions PBAC (#22375)
* feat:add method for getting teamId permissions * add logic and test for zero permsiion strings * fix test by updating mock
This commit is contained in:
1 parent
260f4e1b51
commit
b5911a9a2d
4 files changed
+209
-1
No files matched your search
@@ -44,4 +44,14 @@ export interface IPermissionRepository {
|
||||
teamId: number,
|
||||
resource: Resource
|
||||
): Promise<(CrudAction | CustomAction)[]>;
|
||||
|
||||
/**
|
||||
* Gets all team IDs where the user has a specific permission
|
||||
*/
|
||||
getTeamIdsWithPermission(userId: number, permission: PermissionString): Promise<number[]>;
|
||||
|
||||
/**
|
||||
* Gets all team IDs where the user has all of the specified permissions
|
||||
*/
|
||||
getTeamIdsWithPermissions(userId: number, permissions: PermissionString[]): Promise<number[]>;
|
||||
}
|
||||
@@ -107,6 +107,11 @@ export class PermissionRepository implements IPermissionRepository {
|
||||
}
|
||||
|
||||
async checkRolePermissions(roleId: string, permissions: PermissionString[]): Promise<boolean> {
|
||||
// Validate that permissions array is not empty to prevent privilege escalation
|
||||
if (permissions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const permissionPairs = permissions.map((p) => {
|
||||
const [resource, action] = p.split(".");
|
||||
return { resource, action };
|
||||
@@ -173,4 +178,46 @@ export class PermissionRepository implements IPermissionRepository {
|
||||
});
|
||||
return teamPermissions.map((p) => p.action as CrudAction | CustomAction);
|
||||
}
|
||||
|
||||
async getTeamIdsWithPermission(userId: number, permission: PermissionString): Promise<number[]> {
|
||||
return this.getTeamIdsWithPermissions(userId, [permission]);
|
||||
}
|
||||
|
||||
async getTeamIdsWithPermissions(userId: number, permissions: PermissionString[]): Promise<number[]> {
|
||||
// Validate that permissions array is not empty to prevent privilege escalation
|
||||
if (permissions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const permissionPairs = permissions.map((p) => {
|
||||
const [resource, action] = p.split(".");
|
||||
return { resource, action };
|
||||
});
|
||||
|
||||
const teamsWithPermission = await this.client.$queryRaw<{ teamId: number }[]>`
|
||||
SELECT DISTINCT m."teamId"
|
||||
FROM "Membership" m
|
||||
INNER JOIN "Role" r ON m."customRoleId" = r.id
|
||||
WHERE m."userId" = ${userId}
|
||||
AND m."accepted" = true
|
||||
AND m."customRoleId" IS NOT NULL
|
||||
AND (
|
||||
SELECT COUNT(*)
|
||||
FROM jsonb_array_elements(${JSON.stringify(permissionPairs)}::jsonb) AS required_perm(perm)
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM "RolePermission" rp
|
||||
WHERE rp."roleId" = r.id
|
||||
AND (
|
||||
(rp."resource" = '*' AND rp."action" = '*') OR
|
||||
(rp."resource" = '*' AND rp."action" = required_perm.perm->>'action') OR
|
||||
(rp."resource" = required_perm.perm->>'resource' AND rp."action" = '*') OR
|
||||
(rp."resource" = required_perm.perm->>'resource' AND rp."action" = required_perm.perm->>'action')
|
||||
)
|
||||
)
|
||||
) = ${permissions.length}
|
||||
`;
|
||||
|
||||
return teamsWithPermission.map((team) => team.teamId);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import type { IPermissionRepository } from "../../domain/repositories/IPermissionRepository";
|
||||
import type { PermissionString } from "../../domain/types/permission-registry";
|
||||
import { PermissionCheckService } from "../permission-check.service";
|
||||
import type { PermissionService } from "../permission.service";
|
||||
|
||||
@@ -33,7 +34,9 @@ describe("PermissionCheckService", () => {
|
||||
checkRolePermission: vi.fn(),
|
||||
checkRolePermissions: vi.fn(),
|
||||
getResourcePermissions: vi.fn(),
|
||||
};
|
||||
getTeamIdsWithPermission: vi.fn(),
|
||||
getTeamIdsWithPermissions: vi.fn(),
|
||||
} as MockRepository;
|
||||
|
||||
mockFeaturesRepository = {
|
||||
checkIfTeamHasFeature: vi.fn(),
|
||||
@@ -244,6 +247,41 @@ describe("PermissionCheckService", () => {
|
||||
expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac");
|
||||
expect(mockRepository.checkRolePermissions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false when permissions array is empty", async () => {
|
||||
const membership = {
|
||||
id: 1,
|
||||
teamId: 1,
|
||||
userId: 1,
|
||||
accepted: true,
|
||||
role: "ADMIN" as MembershipRole,
|
||||
customRoleId: "admin_role",
|
||||
disableImpersonation: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
(MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValueOnce(membership);
|
||||
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
|
||||
mockRepository.getMembershipByMembershipId.mockResolvedValueOnce({
|
||||
id: membership.id,
|
||||
teamId: membership.teamId,
|
||||
userId: membership.userId,
|
||||
customRoleId: membership.customRoleId,
|
||||
});
|
||||
mockRepository.getOrgMembership.mockResolvedValueOnce(null);
|
||||
mockRepository.checkRolePermissions.mockResolvedValueOnce(false);
|
||||
|
||||
const result = await service.checkPermissions({
|
||||
userId: 1,
|
||||
teamId: 1,
|
||||
permissions: [],
|
||||
fallbackRoles: ["ADMIN", "OWNER"],
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockRepository.checkRolePermissions).toHaveBeenCalledWith("admin_role", []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserPermissions", () => {
|
||||
@@ -264,4 +302,81 @@ describe("PermissionCheckService", () => {
|
||||
expect(mockRepository.getUserMemberships).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTeamIdsWithPermission", () => {
|
||||
it("should return team IDs where user has the specified permission", async () => {
|
||||
const expectedTeamIds = [1, 2, 3];
|
||||
mockRepository.getTeamIdsWithPermission.mockResolvedValueOnce(expectedTeamIds);
|
||||
|
||||
const result = await service.getTeamIdsWithPermission(1, "eventType.read");
|
||||
|
||||
expect(result).toEqual(expectedTeamIds);
|
||||
expect(mockRepository.getTeamIdsWithPermission).toHaveBeenCalledWith(1, "eventType.read");
|
||||
});
|
||||
|
||||
it("should return empty array when permission validation fails", async () => {
|
||||
mockPermissionService.validatePermission.mockReturnValueOnce({
|
||||
isValid: false,
|
||||
error: "Invalid permissions",
|
||||
});
|
||||
|
||||
const result = await service.getTeamIdsWithPermission(1, "eventType.read");
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRepository.getTeamIdsWithPermission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return empty array and log error when repository throws", async () => {
|
||||
mockRepository.getTeamIdsWithPermission.mockRejectedValueOnce(new Error("Database error"));
|
||||
|
||||
const result = await service.getTeamIdsWithPermission(1, "eventType.read");
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRepository.getTeamIdsWithPermission).toHaveBeenCalledWith(1, "eventType.read");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTeamIdsWithPermissions", () => {
|
||||
it("should return team IDs where user has all specified permissions", async () => {
|
||||
const expectedTeamIds = [1, 2];
|
||||
const permissions: PermissionString[] = ["eventType.read", "eventType.create"];
|
||||
mockRepository.getTeamIdsWithPermissions.mockResolvedValueOnce(expectedTeamIds);
|
||||
|
||||
const result = await service.getTeamIdsWithPermissions(1, permissions);
|
||||
|
||||
expect(result).toEqual(expectedTeamIds);
|
||||
expect(mockRepository.getTeamIdsWithPermissions).toHaveBeenCalledWith(1, permissions);
|
||||
});
|
||||
|
||||
it("should return empty array when permissions validation fails", async () => {
|
||||
mockPermissionService.validatePermissions.mockReturnValueOnce({
|
||||
isValid: false,
|
||||
error: "Invalid permissions",
|
||||
});
|
||||
|
||||
const result = await service.getTeamIdsWithPermissions(1, ["eventType.read"] as PermissionString[]);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRepository.getTeamIdsWithPermissions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return empty array when permissions array is empty", async () => {
|
||||
mockRepository.getTeamIdsWithPermissions.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.getTeamIdsWithPermissions(1, []);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRepository.getTeamIdsWithPermissions).toHaveBeenCalledWith(1, []);
|
||||
});
|
||||
|
||||
it("should return empty array and log error when repository throws", async () => {
|
||||
const permissions: PermissionString[] = ["eventType.read", "eventType.create"];
|
||||
mockRepository.getTeamIdsWithPermissions.mockRejectedValueOnce(new Error("Database error"));
|
||||
|
||||
const result = await service.getTeamIdsWithPermissions(1, permissions);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRepository.getTeamIdsWithPermissions).toHaveBeenCalledWith(1, permissions);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -225,4 +225,40 @@ export class PermissionCheckService {
|
||||
private checkFallbackRoles(userRole: MembershipRole, allowedRoles: MembershipRole[]): boolean {
|
||||
return allowedRoles.includes(userRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all team IDs where the user has a specific permission
|
||||
*/
|
||||
async getTeamIdsWithPermission(userId: number, permission: PermissionString): Promise<number[]> {
|
||||
try {
|
||||
const validationResult = this.permissionService.validatePermission(permission);
|
||||
if (!validationResult.isValid) {
|
||||
this.logger.error(validationResult.error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return await this.repository.getTeamIdsWithPermission(userId, permission);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all team IDs where the user has all of the specified permissions
|
||||
*/
|
||||
async getTeamIdsWithPermissions(userId: number, permissions: PermissionString[]): Promise<number[]> {
|
||||
try {
|
||||
const validationResult = this.permissionService.validatePermissions(permissions);
|
||||
if (!validationResult.isValid) {
|
||||
this.logger.error(validationResult.error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return await this.repository.getTeamIdsWithPermissions(userId, permissions);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user