-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: minimum required roles guard api-v2 (#15576)
* chore: minimum required roles guard api-v2 * fixup! chore: minimum required roles guard api-v2 * fixup! fixup! chore: minimum required roles guard api-v2 * fixup! Merge branch 'chore-roles-guard-api-v2' of github.com:calcom/cal.com into chore-roles-guard-api-v2 * fixup! Merge branch 'chore-roles-guard-api-v2' of github.com:calcom/cal.com into chore-roles-guard-api-v2
- Loading branch information
1 parent
4f944ec
commit 85c7d4e
Showing
12 changed files
with
291 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { MembershipRole } from "@prisma/client"; | ||
|
||
export const SYSTEM_ADMIN_ROLE = "SYSADMIN"; | ||
export const ORG_ROLES = [ | ||
`ORG_${MembershipRole.OWNER}`, | ||
`ORG_${MembershipRole.ADMIN}`, | ||
`ORG_${MembershipRole.MEMBER}`, | ||
] as const; | ||
export const TEAM_ROLES = [ | ||
`TEAM_${MembershipRole.OWNER}`, | ||
`TEAM_${MembershipRole.ADMIN}`, | ||
`TEAM_${MembershipRole.MEMBER}`, | ||
] as const; |
4 changes: 4 additions & 0 deletions
4
apps/api/v2/src/modules/auth/decorators/roles/membership-roles.decorator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { Reflector } from "@nestjs/core"; | ||
import { MembershipRole } from "@prisma/client"; | ||
|
||
export const MembershipRoles = Reflector.createDecorator<MembershipRole[]>(); |
6 changes: 4 additions & 2 deletions
6
apps/api/v2/src/modules/auth/decorators/roles/roles.decorator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import { SYSTEM_ADMIN_ROLE, ORG_ROLES, TEAM_ROLES } from "@/lib/roles/constants"; | ||
import { Reflector } from "@nestjs/core"; | ||
import { MembershipRole } from "@prisma/client"; | ||
|
||
export const Roles = Reflector.createDecorator<MembershipRole[]>(); | ||
export const Roles = Reflector.createDecorator< | ||
(typeof ORG_ROLES)[number] | (typeof TEAM_ROLES)[number] | typeof SYSTEM_ADMIN_ROLE | ||
>(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { ORG_ROLES, TEAM_ROLES, SYSTEM_ADMIN_ROLE } from "@/lib/roles/constants"; | ||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator"; | ||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; | ||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; | ||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger } from "@nestjs/common"; | ||
import { Reflector } from "@nestjs/core"; | ||
import { Request } from "express"; | ||
|
||
import { Team } from "@calcom/prisma/client"; | ||
|
||
@Injectable() | ||
export class RolesGuard implements CanActivate { | ||
private readonly logger = new Logger("RolesGuard Logger"); | ||
constructor(private reflector: Reflector, private membershipRepository: MembershipsRepository) {} | ||
|
||
async canActivate(context: ExecutionContext): Promise<boolean> { | ||
const request = context.switchToHttp().getRequest<Request & { team: Team }>(); | ||
const teamId = request.params.teamId as string; | ||
const orgId = request.params.orgId as string; | ||
const user = request.user as GetUserReturnType; | ||
const allowedRole = this.reflector.get(Roles, context.getHandler()); | ||
|
||
// User is not authenticated | ||
if (!user) { | ||
this.logger.log("User is not authenticated, denying access."); | ||
return false; | ||
} | ||
|
||
// System admin can access everything | ||
if (user.isSystemAdmin) { | ||
this.logger.log(`User (${user.id}) is system admin, allowing access.`); | ||
return true; | ||
} | ||
|
||
// if the required role is SYSTEM_ADMIN_ROLE but user is not system admin, return false | ||
if (allowedRole === SYSTEM_ADMIN_ROLE && !user.isSystemAdmin) { | ||
this.logger.log(`User (${user.id}) is not system admin, denying access.`); | ||
return false; | ||
} | ||
|
||
// Checking the role of the user within the organization | ||
if (Boolean(orgId) && !Boolean(teamId)) { | ||
const membership = await this.membershipRepository.findMembershipByOrgId(Number(orgId), user.id); | ||
if (!membership) { | ||
this.logger.log(`User (${user.id}) is not a member of the organization (${orgId}), denying access.`); | ||
throw new ForbiddenException(`User is not a member of the organization.`); | ||
} | ||
|
||
if (ORG_ROLES.includes(allowedRole as unknown as (typeof ORG_ROLES)[number])) { | ||
return hasMinimumRole({ | ||
checkRole: `ORG_${membership.role}`, | ||
minimumRole: allowedRole, | ||
roles: ORG_ROLES, | ||
}); | ||
} | ||
} | ||
|
||
// Checking the role of the user within the team | ||
if (Boolean(teamId) && !Boolean(orgId)) { | ||
const membership = await this.membershipRepository.findMembershipByTeamId(Number(teamId), user.id); | ||
if (!membership) { | ||
this.logger.log(`User (${user.id}) is not a member of the team (${teamId}), denying access.`); | ||
throw new ForbiddenException(`User is not a member of the team.`); | ||
} | ||
if (TEAM_ROLES.includes(allowedRole as unknown as (typeof TEAM_ROLES)[number])) { | ||
return hasMinimumRole({ | ||
checkRole: `TEAM_${membership.role}`, | ||
minimumRole: allowedRole, | ||
roles: TEAM_ROLES, | ||
}); | ||
} | ||
} | ||
|
||
// Checking the role for team and org, org is above team in term of permissions | ||
if (Boolean(teamId) && Boolean(orgId)) { | ||
const teamMembership = await this.membershipRepository.findMembershipByTeamId(Number(teamId), user.id); | ||
const orgMembership = await this.membershipRepository.findMembershipByOrgId(Number(orgId), user.id); | ||
|
||
if (!orgMembership) { | ||
this.logger.log(`User (${user.id}) is not part of the organization (${orgId}), denying access.`); | ||
throw new ForbiddenException(`User is not part of the organization.`); | ||
} | ||
|
||
// if the role checked is a TEAM role | ||
if (TEAM_ROLES.includes(allowedRole as unknown as (typeof TEAM_ROLES)[number])) { | ||
// if the user is admin or owner of org, allow request because org > team | ||
if (`ORG_${orgMembership.role}` === "ORG_ADMIN" || `ORG_${orgMembership.role}` === "ORG_OWNER") { | ||
return true; | ||
} | ||
|
||
if (!teamMembership) { | ||
this.logger.log( | ||
`User (${user.id}) is not part of the team (${teamId}) and/or, is not an admin nor an owner of the organization (${orgId}).` | ||
); | ||
throw new ForbiddenException( | ||
"User is not part of the team and/or, is not an admin nor an owner of the organization." | ||
); | ||
} | ||
|
||
// if user is not admin nor an owner of org, and is part of the team, then check user team membership role | ||
return hasMinimumRole({ | ||
checkRole: `TEAM_${teamMembership.role}`, | ||
minimumRole: allowedRole, | ||
roles: TEAM_ROLES, | ||
}); | ||
} | ||
|
||
// if allowed role is a ORG ROLE, check org membersip role | ||
if (ORG_ROLES.includes(allowedRole as unknown as (typeof ORG_ROLES)[number])) { | ||
return hasMinimumRole({ | ||
checkRole: `ORG_${orgMembership.role}`, | ||
minimumRole: allowedRole, | ||
roles: ORG_ROLES, | ||
}); | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
} | ||
|
||
type Roles = (typeof ORG_ROLES)[number] | (typeof TEAM_ROLES)[number]; | ||
|
||
type HasMinimumTeamRoleProp = { | ||
checkRole: (typeof TEAM_ROLES)[number]; | ||
minimumRole: string; | ||
roles: typeof TEAM_ROLES; | ||
}; | ||
|
||
type HasMinimumOrgRoleProp = { | ||
checkRole: (typeof ORG_ROLES)[number]; | ||
minimumRole: string; | ||
roles: typeof ORG_ROLES; | ||
}; | ||
|
||
type HasMinimumRoleProp = HasMinimumTeamRoleProp | HasMinimumOrgRoleProp; | ||
|
||
export function hasMinimumRole(props: HasMinimumRoleProp): boolean { | ||
const checkedRoleIndex = props.roles.indexOf(props.checkRole as never); | ||
const requiredRoleIndex = props.roles.indexOf(props.minimumRole as never); | ||
|
||
// minimum role given does not exist | ||
if (checkedRoleIndex === -1 || requiredRoleIndex === -1) { | ||
throw new Error("Invalid role"); | ||
} | ||
|
||
return checkedRoleIndex <= requiredRoleIndex; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.