Files
calendar/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts
T
75465ac4bf feat: PBAC on team members page + org members page to use specific permissi… (#23004)
* PBAC on team members page + org members page to use specific permissions with fallbacks

* Implement checks on edit sheet fetch to check for permisisons

* WIP permission fixes

* fix tests

* WIP

* fix edit mode perms

* fix remove and list view permissions

* fix add

* feat: add pbac permissions for impersonation (#23035)

* registery + migration + i18n

* UI for can impersonate

* impersonation backend

* remove permission from db query

* add missing prisma import

* add resend invitation and edit mode checks for delete/changeRole

* fix canChangeRole

* canChangeRole gate

* fix permission logic

* fix depends on

* push migration

* fix migration

* fix scoped advanced permissions

* use listMembers instead of invite proxy

* remove isOrgAdmin check to rely on pbac

* wait for session to load before navigating

* use event-types testId to ensure session loaded

* use profile page instead of eventTypes

* use correct roles

* fix wait for session

* correct orgs private isFallBackRoles

* set a timeout on waiting for session

* remove adminOrOwner check since its in fallpack

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-09-02 12:52:45 -03:00

122 lines
4.4 KiB
TypeScript

import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { isTeamOwner } from "@calcom/lib/server/queries/teams";
import { TeamService } from "@calcom/lib/server/service/teamService";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import type { TRemoveMemberInputSchema } from "./removeMember.schema";
type RemoveMemberOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
sourceIp?: string;
};
input: TRemoveMemberInputSchema;
};
export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) => {
await checkRateLimitAndThrowError({
identifier: `removeMember.${ctx.user.id}`,
});
const { memberIds, teamIds, isOrg } = input;
// Check PBAC permissions for each team
const hasRemovePermission = await Promise.all(
teamIds.map(async (teamId) => {
// Get user's membership role in this team
const membership = await prisma.membership.findFirst({
where: {
userId: ctx.user.id,
teamId: teamId,
},
select: {
role: true,
},
});
if (!membership) return false;
// Check PBAC permissions for removing team members
const permissions = await getSpecificPermissions({
userId: ctx.user.id,
teamId: teamId,
resource: isOrg ? Resource.Organization : Resource.Team,
userRole: membership.role,
actions: [CustomAction.Remove],
fallbackRoles: {
[CustomAction.Remove]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
});
return permissions[CustomAction.Remove];
})
).then((results) => results.every((result) => result));
// Check if user is trying to remove themselves (allowed for non-owners)
const isRemovingSelf = memberIds.length === 1 && memberIds[0] === ctx.user.id;
// Allow if user has remove permission OR if they're removing themselves
if (!hasRemovePermission && !isRemovingSelf) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// TODO(SEAN): Remove this after PBAC is rolled out.
// Check if any team has PBAC enabled
const featuresRepository = new FeaturesRepository(prisma);
const pbacEnabledForTeams = await Promise.all(
teamIds.map(async (teamId) => await featuresRepository.checkIfTeamHasFeature(teamId, "pbac"))
);
const isAnyTeamPBACEnabled = pbacEnabledForTeams.some((enabled) => enabled);
// Only apply traditional owner-based logic if PBAC is not enabled for any teams
if (!isAnyTeamPBACEnabled) {
// Only a team owner can remove another team owner.
const isAnyMemberOwnerAndCurrentUserNotOwner = await Promise.all(
memberIds.map(async (memberId) => {
const isAnyTeamOwnerAndCurrentUserNotOwner = await Promise.all(
teamIds.map(async (teamId) => {
return (await isTeamOwner(memberId, teamId)) && !(await isTeamOwner(ctx.user.id, teamId));
})
).then((results) => results.some((result) => result));
return isAnyTeamOwnerAndCurrentUserNotOwner;
})
).then((results) => results.some((result) => result));
if (isAnyMemberOwnerAndCurrentUserNotOwner) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Only a team owner can remove another team owner.",
});
}
// Check if user is trying to remove themselves from a team they own (prevent this)
if (isRemovingSelf && hasRemovePermission) {
// Additional check: ensure they're not an owner trying to remove themselves
const isOwnerOfAnyTeam = await Promise.all(
teamIds.map(async (teamId) => await isTeamOwner(ctx.user.id, 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.",
});
}
}
}
await TeamService.removeMembers({ teamIds, userIds: memberIds, isOrg });
};
export default removeMemberHandler;