|
1 | | -import { FeaturesRepository } from "@calcom/features/flags/features.repository"; |
2 | | -import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry"; |
3 | | -import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions"; |
4 | 1 | import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; |
5 | | -import { isTeamOwner } from "@calcom/features/ee/teams/lib/queries"; |
6 | | -import { TeamService } from "@calcom/lib/server/service/teamService"; |
7 | | -import { prisma } from "@calcom/prisma"; |
8 | | -import { MembershipRole } from "@calcom/prisma/enums"; |
9 | | -import type { TrpcSessionUser } from "@calcom/trpc/server/types"; |
10 | 2 |
|
11 | 3 | import { TRPCError } from "@trpc/server"; |
12 | 4 |
|
13 | 5 | import type { TRemoveMemberInputSchema } from "./removeMember.schema"; |
| 6 | +import { RemoveMemberServiceFactory } from "./removeMember/RemoveMemberServiceFactory"; |
14 | 7 |
|
15 | 8 | type RemoveMemberOptions = { |
16 | 9 | ctx: { |
17 | | - user: NonNullable<TrpcSessionUser>; |
18 | | - sourceIp?: string; |
| 10 | + user: { |
| 11 | + id: number; |
| 12 | + organization?: { |
| 13 | + isOrgAdmin: boolean; |
| 14 | + }; |
| 15 | + }; |
19 | 16 | }; |
20 | 17 | input: TRemoveMemberInputSchema; |
21 | 18 | }; |
22 | 19 |
|
23 | | -export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) => { |
| 20 | +export const removeMemberHandler = async ({ |
| 21 | + ctx: { |
| 22 | + user: { id: userId, organization }, |
| 23 | + }, |
| 24 | + input, |
| 25 | +}: RemoveMemberOptions) => { |
24 | 26 | await checkRateLimitAndThrowError({ |
25 | | - identifier: `removeMember.${ctx.user.id}`, |
| 27 | + identifier: `removeMember.${userId}`, |
26 | 28 | }); |
27 | 29 |
|
28 | 30 | const { memberIds, teamIds, isOrg } = input; |
| 31 | + const isOrgAdmin = organization?.isOrgAdmin ?? false; |
| 32 | + |
| 33 | + // Note: This assumes that all teams in the request have the same PBAC setting 9999% chance they do. |
| 34 | + const primaryTeamId = teamIds[0]; |
| 35 | + if (!primaryTeamId) { |
| 36 | + throw new TRPCError({ |
| 37 | + code: "BAD_REQUEST", |
| 38 | + message: "At least one team ID must be provided", |
| 39 | + }); |
| 40 | + } |
29 | 41 |
|
30 | | - // Check PBAC permissions for each team |
31 | | - const hasRemovePermission = await Promise.all( |
32 | | - teamIds.map(async (teamId) => { |
33 | | - // Get user's membership role in this team |
34 | | - const membership = await prisma.membership.findFirst({ |
35 | | - where: { |
36 | | - userId: ctx.user.id, |
37 | | - teamId: teamId, |
38 | | - }, |
39 | | - select: { |
40 | | - role: true, |
41 | | - }, |
42 | | - }); |
43 | | - |
44 | | - if (!membership) return false; |
45 | | - |
46 | | - // Check PBAC permissions for removing team members |
47 | | - const permissions = await getSpecificPermissions({ |
48 | | - userId: ctx.user.id, |
49 | | - teamId: teamId, |
50 | | - resource: isOrg ? Resource.Organization : Resource.Team, |
51 | | - userRole: membership.role, |
52 | | - actions: [CustomAction.Remove], |
53 | | - fallbackRoles: { |
54 | | - [CustomAction.Remove]: { |
55 | | - roles: [MembershipRole.ADMIN, MembershipRole.OWNER], |
56 | | - }, |
57 | | - }, |
58 | | - }); |
59 | | - |
60 | | - return permissions[CustomAction.Remove]; |
61 | | - }) |
62 | | - ).then((results) => results.every((result) => result)); |
| 42 | + // Get the appropriate service based on feature flag |
| 43 | + const service = await RemoveMemberServiceFactory.create(primaryTeamId); |
63 | 44 |
|
64 | | - // Check if user is trying to remove themselves (allowed for non-owners) |
65 | | - const isRemovingSelf = memberIds.length === 1 && memberIds[0] === ctx.user.id; |
| 45 | + const { hasPermission } = await service.checkRemovePermissions({ |
| 46 | + userId, |
| 47 | + isOrgAdmin, |
| 48 | + memberIds, |
| 49 | + teamIds, |
| 50 | + isOrg, |
| 51 | + }); |
66 | 52 |
|
67 | | - // Allow if user has remove permission OR if they're removing themselves |
68 | | - if (!hasRemovePermission && !isRemovingSelf) { |
| 53 | + if (!hasPermission) { |
69 | 54 | throw new TRPCError({ code: "UNAUTHORIZED" }); |
70 | 55 | } |
71 | 56 |
|
72 | | - // TODO(SEAN): Remove this after PBAC is rolled out. |
73 | | - // Check if any team has PBAC enabled |
74 | | - const featuresRepository = new FeaturesRepository(prisma); |
75 | | - const pbacEnabledForTeams = await Promise.all( |
76 | | - teamIds.map(async (teamId) => await featuresRepository.checkIfTeamHasFeature(teamId, "pbac")) |
| 57 | + await service.validateRemoval( |
| 58 | + { |
| 59 | + userId, |
| 60 | + isOrgAdmin, |
| 61 | + memberIds, |
| 62 | + teamIds, |
| 63 | + isOrg, |
| 64 | + }, |
| 65 | + hasPermission |
77 | 66 | ); |
78 | | - const isAnyTeamPBACEnabled = pbacEnabledForTeams.some((enabled) => enabled); |
79 | | - |
80 | | - // Only apply traditional owner-based logic if PBAC is not enabled for any teams |
81 | | - if (!isAnyTeamPBACEnabled) { |
82 | | - // Only a team owner can remove another team owner. |
83 | | - const isAnyMemberOwnerAndCurrentUserNotOwner = await Promise.all( |
84 | | - memberIds.map(async (memberId) => { |
85 | | - const isAnyTeamOwnerAndCurrentUserNotOwner = await Promise.all( |
86 | | - teamIds.map(async (teamId) => { |
87 | | - return (await isTeamOwner(memberId, teamId)) && !(await isTeamOwner(ctx.user.id, teamId)); |
88 | | - }) |
89 | | - ).then((results) => results.some((result) => result)); |
90 | | - |
91 | | - return isAnyTeamOwnerAndCurrentUserNotOwner; |
92 | | - }) |
93 | | - ).then((results) => results.some((result) => result)); |
94 | | - |
95 | | - if (isAnyMemberOwnerAndCurrentUserNotOwner) { |
96 | | - throw new TRPCError({ |
97 | | - code: "UNAUTHORIZED", |
98 | | - message: "Only a team owner can remove another team owner.", |
99 | | - }); |
100 | | - } |
101 | | - |
102 | | - // Check if user is trying to remove themselves from a team they own (prevent this) |
103 | | - if (isRemovingSelf && hasRemovePermission) { |
104 | | - // Additional check: ensure they're not an owner trying to remove themselves |
105 | | - const isOwnerOfAnyTeam = await Promise.all( |
106 | | - teamIds.map(async (teamId) => await isTeamOwner(ctx.user.id, teamId)) |
107 | | - ).then((results) => results.some((result) => result)); |
108 | | - |
109 | | - if (isOwnerOfAnyTeam) { |
110 | | - throw new TRPCError({ |
111 | | - code: "FORBIDDEN", |
112 | | - message: "You can not remove yourself from a team you own.", |
113 | | - }); |
114 | | - } |
115 | | - } |
116 | | - } |
117 | 67 |
|
118 | | - await TeamService.removeMembers({ teamIds, userIds: memberIds, isOrg }); |
| 68 | + // Perform the removal |
| 69 | + await service.removeMembers(memberIds, teamIds, isOrg); |
119 | 70 | }; |
120 | 71 |
|
121 | 72 | export default removeMemberHandler; |
0 commit comments