Skip to content

Commit ccd7fdf

Browse files
authored
refactor role manage (calcom#24146)
1 parent f92e59e commit ccd7fdf

4 files changed

Lines changed: 262 additions & 251 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { isTeamOwner } from "@calcom/features/ee/teams/lib/queries";
2+
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
3+
import { prisma } from "@calcom/prisma";
4+
import type { Membership } from "@calcom/prisma/client";
5+
import { MembershipRole } from "@calcom/prisma/enums";
6+
7+
import { RoleManagementError, RoleManagementErrorCode } from "../domain/errors/role-management.error";
8+
import type { IRoleManager } from "./role-manager.interface";
9+
10+
export class LegacyRoleManager implements IRoleManager {
11+
public isPBACEnabled = false;
12+
13+
protected async validateRoleChange(
14+
userId: number,
15+
teamId: number,
16+
memberId: number,
17+
newRole: MembershipRole | string,
18+
memberships: Membership[]
19+
): Promise<void> {
20+
// Only validate for traditional MembershipRole values
21+
if (typeof newRole !== "string" || !Object.values(MembershipRole).includes(newRole as MembershipRole)) {
22+
return;
23+
}
24+
25+
const targetMembership = memberships.find((m) => m.userId === memberId);
26+
const myMembership = memberships.find((m) => m.userId === userId);
27+
const teamOwners = memberships.filter((m) => m.role === MembershipRole.OWNER);
28+
const teamHasMoreThanOneOwner = teamOwners.length > 1;
29+
30+
if (!targetMembership) {
31+
throw new RoleManagementError("Target membership not found", RoleManagementErrorCode.UNAUTHORIZED);
32+
}
33+
34+
// Only owners can award owner role
35+
if (newRole === MembershipRole.OWNER && !(await isTeamOwner(userId, teamId))) {
36+
throw new RoleManagementError("Only owners can award owner role", RoleManagementErrorCode.UNAUTHORIZED);
37+
}
38+
39+
// Admins cannot change the role of an owner
40+
if (myMembership?.role === MembershipRole.ADMIN && targetMembership?.role === MembershipRole.OWNER) {
41+
throw new RoleManagementError(
42+
"You can not change the role of an owner if you are an admin.",
43+
RoleManagementErrorCode.UNAUTHORIZED
44+
);
45+
}
46+
47+
// Cannot change the role of the only owner
48+
if (targetMembership?.role === MembershipRole.OWNER && !teamHasMoreThanOneOwner) {
49+
throw new RoleManagementError(
50+
"You can not change the role of the only owner of a team.",
51+
RoleManagementErrorCode.UNAUTHORIZED
52+
);
53+
}
54+
55+
// Admins cannot promote themselves to a higher role (except to MEMBER which is a demotion)
56+
if (
57+
myMembership?.role === MembershipRole.ADMIN &&
58+
memberId === userId &&
59+
newRole !== MembershipRole.MEMBER
60+
) {
61+
throw new RoleManagementError(
62+
"You can not change yourself to a higher role.",
63+
RoleManagementErrorCode.UNAUTHORIZED
64+
);
65+
}
66+
}
67+
68+
async checkPermissionToChangeRole(
69+
userId: number,
70+
targetId: number,
71+
scope: "org" | "team",
72+
memberId?: number,
73+
newRole?: MembershipRole | string
74+
): Promise<void> {
75+
let hasPermission = false;
76+
if (scope === "team") {
77+
const team = await prisma.membership.findFirst({
78+
where: {
79+
userId,
80+
teamId: targetId,
81+
accepted: true,
82+
OR: [{ role: "ADMIN" }, { role: "OWNER" }],
83+
},
84+
});
85+
hasPermission = !!team;
86+
} else {
87+
hasPermission = !!(await isOrganisationAdmin(userId, targetId));
88+
}
89+
90+
// Only OWNER/ADMIN can update role
91+
if (!hasPermission) {
92+
throw new RoleManagementError(
93+
"Only owners or admin can update roles",
94+
RoleManagementErrorCode.UNAUTHORIZED
95+
);
96+
}
97+
98+
// Additional validation for team role changes in legacy mode
99+
if (scope === "team" && memberId && newRole) {
100+
const memberships = await prisma.membership.findMany({
101+
where: {
102+
teamId: targetId,
103+
accepted: true,
104+
},
105+
});
106+
await this.validateRoleChange(userId, targetId, memberId, newRole, memberships);
107+
}
108+
}
109+
110+
async assignRole(
111+
userId: number,
112+
organizationId: number,
113+
role: MembershipRole | string,
114+
// Used in other implementation
115+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
116+
_membershipId: number
117+
): Promise<void> {
118+
await prisma.membership.update({
119+
where: {
120+
userId_teamId: {
121+
userId,
122+
teamId: organizationId,
123+
},
124+
},
125+
data: {
126+
role: role as MembershipRole,
127+
},
128+
});
129+
}
130+
131+
// Used in other implementation
132+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
133+
async getAllRoles(_organizationId: number): Promise<{ id: string; name: string }[]> {
134+
return [
135+
{ id: MembershipRole.OWNER, name: "Owner" },
136+
{ id: MembershipRole.ADMIN, name: "Admin" },
137+
{ id: MembershipRole.MEMBER, name: "Member" },
138+
];
139+
}
140+
141+
// Used in other implementation
142+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
143+
async getTeamRoles(_teamId: number): Promise<{ id: string; name: string }[]> {
144+
return [
145+
{ id: MembershipRole.OWNER, name: "Owner" },
146+
{ id: MembershipRole.ADMIN, name: "Admin" },
147+
{ id: MembershipRole.MEMBER, name: "Member" },
148+
];
149+
}
150+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { MembershipRole } from "@calcom/prisma/enums";
2+
3+
import { RoleManagementError, RoleManagementErrorCode } from "../domain/errors/role-management.error";
4+
import { DEFAULT_ROLE_IDS } from "../lib/constants";
5+
import type { IRoleManager } from "./role-manager.interface";
6+
import { PermissionCheckService } from "./permission-check.service";
7+
import { RoleService } from "./role.service";
8+
9+
export class PBACRoleManager implements IRoleManager {
10+
public isPBACEnabled = true;
11+
12+
constructor(
13+
private readonly roleService: RoleService,
14+
private readonly permissionCheckService: PermissionCheckService
15+
) {}
16+
17+
async checkPermissionToChangeRole(
18+
userId: number,
19+
targetId: number,
20+
scope: "org" | "team",
21+
// Not required for this instance
22+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
23+
_memberId?: number,
24+
// Not required for this instance
25+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26+
_newRole?: MembershipRole | string
27+
): Promise<void> {
28+
const hasPermission = await this.permissionCheckService.checkPermission({
29+
userId,
30+
teamId: targetId,
31+
permission: scope === "team" ? "team.changeMemberRole" : "organization.changeMemberRole",
32+
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
33+
});
34+
35+
if (!hasPermission) {
36+
throw new RoleManagementError(
37+
"You do not have permission to change roles",
38+
RoleManagementErrorCode.UNAUTHORIZED
39+
);
40+
}
41+
}
42+
43+
async assignRole(
44+
_userId: number,
45+
organizationId: number,
46+
role: MembershipRole | string,
47+
membershipId: number
48+
): Promise<void> {
49+
// Check if role is one of the default MembershipRole enum values
50+
const isDefaultRole = role in DEFAULT_ROLE_IDS;
51+
52+
// Also check if the role is a default role ID value
53+
const isDefaultRoleId = Object.values(DEFAULT_ROLE_IDS).includes(role as any);
54+
55+
if (isDefaultRole) {
56+
// Handle enum values like MembershipRole.ADMIN
57+
await this.roleService.assignRoleToMember(DEFAULT_ROLE_IDS[role as MembershipRole], membershipId);
58+
} else if (isDefaultRoleId) {
59+
// Handle default role IDs like "admin_role"
60+
await this.roleService.assignRoleToMember(role as string, membershipId);
61+
} else {
62+
// Handle custom roles
63+
const roleExists = await this.roleService.roleBelongsToTeam(role as string, organizationId);
64+
if (!roleExists) {
65+
throw new RoleManagementError(
66+
"You do not have access to this role",
67+
RoleManagementErrorCode.INVALID_ROLE
68+
);
69+
}
70+
await this.roleService.assignRoleToMember(role as string, membershipId);
71+
}
72+
}
73+
74+
async getAllRoles(organizationId: number): Promise<{ id: string; name: string }[]> {
75+
const roles = await this.roleService.getTeamRoles(organizationId);
76+
return roles.map((role) => ({
77+
id: role.id,
78+
name: role.name,
79+
}));
80+
}
81+
82+
async getTeamRoles(teamId: number): Promise<{ id: string; name: string }[]> {
83+
const roles = await this.roleService.getTeamRoles(teamId);
84+
return roles.map((role) => ({
85+
id: role.id,
86+
name: role.name,
87+
}));
88+
}
89+
}

0 commit comments

Comments
 (0)