Skip to content

Commit 7e6d94c

Browse files
authored
fix: team admin/owner can't change others member role (calcom#23161)
* fix: team admin/owner can't change member role * update * Update role-management.factory.ts * address review
1 parent e18b790 commit 7e6d94c

4 files changed

Lines changed: 21 additions & 16 deletions

File tree

packages/features/pbac/services/__tests__/role-management.factory.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,15 @@ describe("RoleManagementFactory", () => {
110110
it("should allow role change when user has permission", async () => {
111111
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
112112
const manager = await factory.createRoleManager(organizationId);
113-
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
113+
await expect(
114+
manager.checkPermissionToChangeRole(userId, organizationId, "org")
115+
).resolves.not.toThrow();
114116
});
115117

116118
it("should throw UNAUTHORIZED when user lacks permission", async () => {
117119
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
118120
const manager = await factory.createRoleManager(organizationId);
119-
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
121+
await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
120122
new RoleManagementError(
121123
"You do not have permission to change roles",
122124
RoleManagementErrorCode.UNAUTHORIZED
@@ -193,13 +195,15 @@ describe("RoleManagementFactory", () => {
193195
customRoleId: null,
194196
});
195197
const manager = await factory.createRoleManager(organizationId);
196-
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
198+
await expect(
199+
manager.checkPermissionToChangeRole(userId, organizationId, "org")
200+
).resolves.not.toThrow();
197201
});
198202

199203
it("should throw UNAUTHORIZED when user is not owner", async () => {
200204
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
201205
const manager = await factory.createRoleManager(organizationId);
202-
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
206+
await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
203207
new RoleManagementError(
204208
"Only owners or admin can update roles",
205209
RoleManagementErrorCode.UNAUTHORIZED

packages/features/pbac/services/role-management.factory.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
22
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
3+
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
34
import { prisma } from "@calcom/prisma";
45
import { MembershipRole } from "@calcom/prisma/enums";
56

@@ -10,7 +11,7 @@ import { RoleService } from "./role.service";
1011

1112
interface IRoleManager {
1213
isPBACEnabled: boolean;
13-
checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void>;
14+
checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void>;
1415
assignRole(
1516
userId: number,
1617
organizationId: number,
@@ -29,11 +30,11 @@ class PBACRoleManager implements IRoleManager {
2930
private readonly permissionCheckService: PermissionCheckService
3031
) {}
3132

32-
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
33+
async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
3334
const hasPermission = await this.permissionCheckService.checkPermission({
3435
userId,
35-
teamId: organizationId,
36-
permission: "organization.changeMemberRole",
36+
teamId: targetId,
37+
permission: scope === "team" ? "team.changeMemberRole" : "organization.changeMemberRole",
3738
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
3839
});
3940

@@ -95,9 +96,11 @@ class PBACRoleManager implements IRoleManager {
9596

9697
class LegacyRoleManager implements IRoleManager {
9798
public isPBACEnabled = false;
98-
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
99-
const membership = await isOrganisationAdmin(userId, organizationId);
100-
99+
async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
100+
const membership =
101+
scope === "team"
102+
? !!(await isTeamAdmin(userId, targetId))
103+
: !!(await isOrganisationAdmin(userId, targetId));
101104
// Only OWNER/ADMIN can update role
102105
if (!membership) {
103106
throw new RoleManagementError(

packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
4747
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(organizationId);
4848

4949
try {
50-
await roleManager.checkPermissionToChangeRole(userId, organizationId);
50+
await roleManager.checkPermissionToChangeRole(userId, organizationId, "org");
5151
} catch (error) {
5252
if (error instanceof RoleManagementError) {
5353
throw new TRPCError({ code: "UNAUTHORIZED", message: error.message });

packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
2-
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
2+
import { isTeamOwner } from "@calcom/lib/server/queries/teams";
33
import { TeamRepository } from "@calcom/lib/server/repository/team";
44
import { prisma } from "@calcom/prisma";
55
import { MembershipRole } from "@calcom/prisma/enums";
@@ -32,7 +32,7 @@ export const changeMemberRoleHandler = async ({ ctx, input }: ChangeMemberRoleOp
3232

3333
// Check permission to change roles
3434
try {
35-
await roleManager.checkPermissionToChangeRole(ctx.user.id, organizationId);
35+
await roleManager.checkPermissionToChangeRole(ctx.user.id, input.teamId, "team");
3636
} catch (error) {
3737
throw new TRPCError({
3838
code: "UNAUTHORIZED",
@@ -45,8 +45,6 @@ export const changeMemberRoleHandler = async ({ ctx, input }: ChangeMemberRoleOp
4545
typeof input.role === "string" &&
4646
Object.values(MembershipRole).includes(input.role as MembershipRole)
4747
) {
48-
// Traditional role assignment logic
49-
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
5048
// Only owners can award owner role.
5149
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
5250
throw new TRPCError({ code: "UNAUTHORIZED" });

0 commit comments

Comments
 (0)