Files
calendar/packages/trpc/server/routers/viewer/teams/removeMember/PBACRemoveMemberService.ts
T
Eunjae LeeGitHubeunjae@cal.com <hey@eunjae.dev>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f796802ed3 feat: add fallbackRoles parameter to getTeamIdsWithPermissions method (#24042)
* feat: add fallbackRoles parameter to getTeamIdsWithPermissions method

- Add fallbackRoles parameter to method signature in interface and implementation
- Implement second query for teams without PBAC where user has fallback roles
- Combine and deduplicate results from both PBAC-enabled and fallback role teams
- Supports fallback to role-based permissions when PBAC is disabled
- Fix linting issue in getUserMemberships method by replacing include with select

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix usages

* revert some change

* fix unit tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-25 11:10:41 +01:00

63 lines
2.4 KiB
TypeScript

import * as teamQueries from "@calcom/features/ee/teams/lib/queries";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import { BaseRemoveMemberService } from "./BaseRemoveMemberService";
import type { RemoveMemberContext, RemoveMemberPermissionResult } from "./IRemoveMemberService";
export class PBACRemoveMemberService extends BaseRemoveMemberService {
private permissionService = new PermissionCheckService();
async checkRemovePermissions(context: RemoveMemberContext): Promise<RemoveMemberPermissionResult> {
const { userId, teamIds, isOrg } = context;
const resource = isOrg ? Resource.Organization : Resource.Team;
const removePermission = PermissionMapper.toPermissionString({
resource,
action: CustomAction.Remove,
});
const teamsWithPermission = await this.permissionService.getTeamIdsWithPermission({
userId,
permission: removePermission,
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
// Convert to Set for O(1) lookup
const teamsWithPermissionSet = new Set(teamsWithPermission);
// Check if user has permission for ALL requested teams
const hasPermission = teamIds.every((teamId) => teamsWithPermissionSet.has(teamId));
return {
hasPermission,
};
}
async validateRemoval(context: RemoveMemberContext, hasPermission: boolean): Promise<void> {
const { userId, memberIds, teamIds } = context;
const isRemovingSelf = memberIds.length === 1 && memberIds[0] === userId;
/**
* TODO: Figure out the best way to prevent someone bricking a team
* by removing all people with updateRole permissions
*/
if (isRemovingSelf && hasPermission) {
const isOwnerOfAnyTeam = await Promise.all(
teamIds.map(async (teamId) => await teamQueries.isTeamOwner(userId, teamId))
).then((results) => results.some((result) => result));
if (isOwnerOfAnyTeam) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can not remove yourself from a team you own.",
});
}
}
}
}