* 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>
122 lines
4.4 KiB
TypeScript
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;
|