Files
calendar/packages/features/pbac/lib/resource-permissions.ts
T
Joe Au-YeungGitHubjoe@cal.com <j.auyeung419@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f6d3b11582 refactor: Membership.findByUserIdAndTeamId to instance method (#26628)
* Make  and instance method

* Refactor files

* fix: update MembershipRepository mocks to use vi.hoisted() for proper hoisting

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 22:37:42 -03:00

214 lines
5.6 KiB
TypeScript

import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { PermissionMapper } from "../domain/mappers/PermissionMapper";
import type { CustomAction } from "../domain/types/permission-registry";
import { CrudAction, Resource } from "../domain/types/permission-registry";
import { PermissionCheckService } from "../services/permission-check.service";
interface RoleMapping {
roles: MembershipRole[];
condition?: (userRole: MembershipRole) => boolean;
}
interface FallbackRoleConfig {
read?: RoleMapping;
create?: RoleMapping;
update?: RoleMapping;
delete?: RoleMapping;
}
interface ResourcePermissionsOptions {
userId: number;
teamId: number;
resource: Resource;
userRole: MembershipRole;
fallbackRoles?: FallbackRoleConfig;
}
interface ResourcePermissions {
canRead: boolean;
canEdit: boolean;
canDelete: boolean;
canCreate: boolean;
}
const checkRoleAccess = (userRole: MembershipRole, mapping?: RoleMapping): boolean => {
if (!mapping) return false;
const { roles, condition } = mapping;
const hasRole = roles.includes(userRole);
if (condition) {
return hasRole && condition(userRole);
}
return hasRole;
};
export const getResourcePermissions = async ({
userId,
teamId,
resource,
userRole,
fallbackRoles = {},
}: ResourcePermissionsOptions): Promise<ResourcePermissions> => {
const featureRepo = new FeaturesRepository(prisma);
const permissionService = new PermissionCheckService();
const pbacEnabled = await featureRepo.checkIfTeamHasFeature(teamId, "pbac");
// If PBAC is disabled, use fallback role configuration
if (!pbacEnabled) {
return {
canRead: checkRoleAccess(userRole, fallbackRoles.read),
canCreate: checkRoleAccess(userRole, fallbackRoles.create),
canEdit: checkRoleAccess(userRole, fallbackRoles.update),
canDelete: checkRoleAccess(userRole, fallbackRoles.delete),
};
}
// PBAC is enabled, get permissions from the service
const resourcePermissions = await permissionService.getResourcePermissions({
userId,
teamId,
resource,
});
const roleActions = PermissionMapper.toActionMap(resourcePermissions, resource);
return {
canRead: roleActions[CrudAction.Read] ?? false,
canEdit: roleActions[CrudAction.Update] ?? false,
canDelete: roleActions[CrudAction.Delete] ?? false,
canCreate: roleActions[CrudAction.Create] ?? false,
};
};
// Enhanced function to get specific custom action permissions
type ActionType = CrudAction | CustomAction;
interface SpecificActionMapping {
[key: string]: RoleMapping;
}
interface SpecificPermissionsOptions {
userId: number;
teamId: number;
resource: Resource;
userRole: MembershipRole;
actions: ActionType[];
fallbackRoles?: SpecificActionMapping;
}
export const getSpecificPermissions = async ({
userId,
teamId,
resource,
userRole,
actions,
fallbackRoles = {},
}: SpecificPermissionsOptions): Promise<Record<string, boolean>> => {
const featureRepo = new FeaturesRepository(prisma);
const permissionService = new PermissionCheckService();
const pbacEnabled = await featureRepo.checkIfTeamHasFeature(teamId, "pbac");
// If PBAC is disabled, use fallback role configuration
if (!pbacEnabled) {
const permissions: Record<string, boolean> = {};
for (const action of actions) {
permissions[action] = checkRoleAccess(userRole, fallbackRoles[action]);
}
return permissions;
}
// PBAC is enabled, get permissions from the service
const resourcePermissions = await permissionService.getResourcePermissions({
userId,
teamId,
resource,
});
const roleActions = PermissionMapper.toActionMap(resourcePermissions, resource);
const permissions: Record<string, boolean> = {};
for (const action of actions) {
permissions[action] = roleActions[action] ?? false;
}
return permissions;
};
export async function getRoutingFormPermissions({
userId,
formUserId,
formTeamId,
formTeamParentId,
}: {
userId: number;
formUserId: number;
formTeamId: number | null;
formTeamParentId: number | null;
}): Promise<ResourcePermissions | null> {
if (!formTeamId) {
if (formUserId !== userId) {
return null;
}
return {
canCreate: true,
canRead: true,
canEdit: true,
canDelete: true,
};
}
const membershipRepository = new MembershipRepository();
const membership = await membershipRepository.findUniqueByUserIdAndTeamId({
userId,
teamId: formTeamId,
});
let isParentOrgAdmin = false;
if (!membership && formTeamParentId) {
const parentOrgMembership = await MembershipRepository.getAdminOrOwnerMembership(
userId,
formTeamParentId
);
isParentOrgAdmin = !!parentOrgMembership;
}
if (!membership && !isParentOrgAdmin) {
return null;
}
if (!membership && isParentOrgAdmin) {
return {
canCreate: true,
canRead: true,
canEdit: true,
canDelete: true,
};
}
if (membership) {
return await getResourcePermissions({
userId,
teamId: formTeamId,
resource: Resource.RoutingForm,
userRole: membership.role,
fallbackRoles: {
read: { roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER] },
create: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER] },
update: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER] },
delete: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER] },
},
});
}
return null;
}