|
| 1 | +import { ORG_ROLES, TEAM_ROLES, SYSTEM_ADMIN_ROLE } from "@/lib/roles/constants"; |
| 2 | +import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator"; |
| 3 | +import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; |
| 4 | +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; |
| 5 | +import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger } from "@nestjs/common"; |
| 6 | +import { Reflector } from "@nestjs/core"; |
| 7 | +import { Request } from "express"; |
| 8 | + |
| 9 | +import { Team } from "@calcom/prisma/client"; |
| 10 | + |
| 11 | +@Injectable() |
| 12 | +export class RolesGuard implements CanActivate { |
| 13 | + private readonly logger = new Logger("RolesGuard Logger"); |
| 14 | + constructor(private reflector: Reflector, private membershipRepository: MembershipsRepository) {} |
| 15 | + |
| 16 | + async canActivate(context: ExecutionContext): Promise<boolean> { |
| 17 | + const request = context.switchToHttp().getRequest<Request & { team: Team }>(); |
| 18 | + const teamId = request.params.teamId as string; |
| 19 | + const orgId = request.params.orgId as string; |
| 20 | + const user = request.user as GetUserReturnType; |
| 21 | + const allowedRole = this.reflector.get(Roles, context.getHandler()); |
| 22 | + |
| 23 | + // User is not authenticated |
| 24 | + if (!user) { |
| 25 | + this.logger.log("User is not authenticated, denying access."); |
| 26 | + return false; |
| 27 | + } |
| 28 | + |
| 29 | + // System admin can access everything |
| 30 | + if (user.isSystemAdmin) { |
| 31 | + this.logger.log(`User (${user.id}) is system admin, allowing access.`); |
| 32 | + return true; |
| 33 | + } |
| 34 | + |
| 35 | + // if the required role is SYSTEM_ADMIN_ROLE but user is not system admin, return false |
| 36 | + if (allowedRole === SYSTEM_ADMIN_ROLE && !user.isSystemAdmin) { |
| 37 | + this.logger.log(`User (${user.id}) is not system admin, denying access.`); |
| 38 | + return false; |
| 39 | + } |
| 40 | + |
| 41 | + // Checking the role of the user within the organization |
| 42 | + if (Boolean(orgId) && !Boolean(teamId)) { |
| 43 | + const membership = await this.membershipRepository.findMembershipByOrgId(Number(orgId), user.id); |
| 44 | + if (!membership) { |
| 45 | + this.logger.log(`User (${user.id}) is not a member of the organization (${orgId}), denying access.`); |
| 46 | + throw new ForbiddenException(`User is not a member of the organization.`); |
| 47 | + } |
| 48 | + |
| 49 | + if (ORG_ROLES.includes(allowedRole as unknown as (typeof ORG_ROLES)[number])) { |
| 50 | + return hasMinimumRole({ |
| 51 | + checkRole: `ORG_${membership.role}`, |
| 52 | + minimumRole: allowedRole, |
| 53 | + roles: ORG_ROLES, |
| 54 | + }); |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + // Checking the role of the user within the team |
| 59 | + if (Boolean(teamId) && !Boolean(orgId)) { |
| 60 | + const membership = await this.membershipRepository.findMembershipByTeamId(Number(teamId), user.id); |
| 61 | + if (!membership) { |
| 62 | + this.logger.log(`User (${user.id}) is not a member of the team (${teamId}), denying access.`); |
| 63 | + throw new ForbiddenException(`User is not a member of the team.`); |
| 64 | + } |
| 65 | + if (TEAM_ROLES.includes(allowedRole as unknown as (typeof TEAM_ROLES)[number])) { |
| 66 | + return hasMinimumRole({ |
| 67 | + checkRole: `TEAM_${membership.role}`, |
| 68 | + minimumRole: allowedRole, |
| 69 | + roles: TEAM_ROLES, |
| 70 | + }); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + // Checking the role for team and org, org is above team in term of permissions |
| 75 | + if (Boolean(teamId) && Boolean(orgId)) { |
| 76 | + const teamMembership = await this.membershipRepository.findMembershipByTeamId(Number(teamId), user.id); |
| 77 | + const orgMembership = await this.membershipRepository.findMembershipByOrgId(Number(orgId), user.id); |
| 78 | + |
| 79 | + if (!orgMembership) { |
| 80 | + this.logger.log(`User (${user.id}) is not part of the organization (${orgId}), denying access.`); |
| 81 | + throw new ForbiddenException(`User is not part of the organization.`); |
| 82 | + } |
| 83 | + |
| 84 | + // if the role checked is a TEAM role |
| 85 | + if (TEAM_ROLES.includes(allowedRole as unknown as (typeof TEAM_ROLES)[number])) { |
| 86 | + // if the user is admin or owner of org, allow request because org > team |
| 87 | + if (`ORG_${orgMembership.role}` === "ORG_ADMIN" || `ORG_${orgMembership.role}` === "ORG_OWNER") { |
| 88 | + return true; |
| 89 | + } |
| 90 | + |
| 91 | + if (!teamMembership) { |
| 92 | + this.logger.log( |
| 93 | + `User (${user.id}) is not part of the team (${teamId}) and/or, is not an admin nor an owner of the organization (${orgId}).` |
| 94 | + ); |
| 95 | + throw new ForbiddenException( |
| 96 | + "User is not part of the team and/or, is not an admin nor an owner of the organization." |
| 97 | + ); |
| 98 | + } |
| 99 | + |
| 100 | + // if user is not admin nor an owner of org, and is part of the team, then check user team membership role |
| 101 | + return hasMinimumRole({ |
| 102 | + checkRole: `TEAM_${teamMembership.role}`, |
| 103 | + minimumRole: allowedRole, |
| 104 | + roles: TEAM_ROLES, |
| 105 | + }); |
| 106 | + } |
| 107 | + |
| 108 | + // if allowed role is a ORG ROLE, check org membersip role |
| 109 | + if (ORG_ROLES.includes(allowedRole as unknown as (typeof ORG_ROLES)[number])) { |
| 110 | + return hasMinimumRole({ |
| 111 | + checkRole: `ORG_${orgMembership.role}`, |
| 112 | + minimumRole: allowedRole, |
| 113 | + roles: ORG_ROLES, |
| 114 | + }); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + return false; |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +type Roles = (typeof ORG_ROLES)[number] | (typeof TEAM_ROLES)[number]; |
| 123 | + |
| 124 | +type HasMinimumTeamRoleProp = { |
| 125 | + checkRole: (typeof TEAM_ROLES)[number]; |
| 126 | + minimumRole: string; |
| 127 | + roles: typeof TEAM_ROLES; |
| 128 | +}; |
| 129 | + |
| 130 | +type HasMinimumOrgRoleProp = { |
| 131 | + checkRole: (typeof ORG_ROLES)[number]; |
| 132 | + minimumRole: string; |
| 133 | + roles: typeof ORG_ROLES; |
| 134 | +}; |
| 135 | + |
| 136 | +type HasMinimumRoleProp = HasMinimumTeamRoleProp | HasMinimumOrgRoleProp; |
| 137 | + |
| 138 | +export function hasMinimumRole(props: HasMinimumRoleProp): boolean { |
| 139 | + const checkedRoleIndex = props.roles.indexOf(props.checkRole as never); |
| 140 | + const requiredRoleIndex = props.roles.indexOf(props.minimumRole as never); |
| 141 | + |
| 142 | + // minimum role given does not exist |
| 143 | + if (checkedRoleIndex === -1 || requiredRoleIndex === -1) { |
| 144 | + throw new Error("Invalid role"); |
| 145 | + } |
| 146 | + |
| 147 | + return checkedRoleIndex <= requiredRoleIndex; |
| 148 | +} |
0 commit comments