chore: add caching apiv2 roles guard (#15694)

This commit is contained in:
Morgan
2024-07-09 09:39:06 +00:00
committed by GitHub
parent 5f8d090d3c
commit 3f9d1c1abf
3 changed files with 57 additions and 18 deletions
@@ -2,6 +2,7 @@ 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 { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Request } from "express";
@@ -11,7 +12,11 @@ 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) {}
constructor(
private reflector: Reflector,
private membershipRepository: MembershipsRepository,
private readonly redisService: RedisService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
@@ -19,27 +24,37 @@ export class RolesGuard implements CanActivate {
const orgId = request.params.orgId as string;
const user = request.user as GetUserReturnType;
const allowedRole = this.reflector.get(Roles, context.getHandler());
const REDIS_CACHE_KEY = `apiv2:user:${user.id ?? "none"}:org:${orgId ?? "none"}:team:${
teamId ?? "none"
}:guard:roles:${allowedRole}`;
const cachedAccess = JSON.parse((await this.redisService.redis.get(REDIS_CACHE_KEY)) ?? "false");
if (cachedAccess) {
return cachedAccess;
}
let canAccess = false;
// User is not authenticated
if (!user) {
this.logger.log("User is not authenticated, denying access.");
return false;
canAccess = false;
}
// System admin can access everything
if (user.isSystemAdmin) {
else if (user.isSystemAdmin) {
this.logger.log(`User (${user.id}) is system admin, allowing access.`);
return true;
canAccess = true;
}
// if the required role is SYSTEM_ADMIN_ROLE but user is not system admin, return false
if (allowedRole === SYSTEM_ADMIN_ROLE && !user.isSystemAdmin) {
else if (allowedRole === SYSTEM_ADMIN_ROLE && !user.isSystemAdmin) {
this.logger.log(`User (${user.id}) is not system admin, denying access.`);
return false;
canAccess = false;
}
// Checking the role of the user within the organization
if (Boolean(orgId) && !Boolean(teamId)) {
else 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.`);
@@ -47,7 +62,7 @@ export class RolesGuard implements CanActivate {
}
if (ORG_ROLES.includes(allowedRole as unknown as (typeof ORG_ROLES)[number])) {
return hasMinimumRole({
canAccess = hasMinimumRole({
checkRole: `ORG_${membership.role}`,
minimumRole: allowedRole,
roles: ORG_ROLES,
@@ -56,14 +71,14 @@ export class RolesGuard implements CanActivate {
}
// Checking the role of the user within the team
if (Boolean(teamId) && !Boolean(orgId)) {
else 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({
canAccess = hasMinimumRole({
checkRole: `TEAM_${membership.role}`,
minimumRole: allowedRole,
roles: TEAM_ROLES,
@@ -72,7 +87,7 @@ export class RolesGuard implements CanActivate {
}
// Checking the role for team and org, org is above team in term of permissions
if (Boolean(teamId) && Boolean(orgId)) {
else 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);
@@ -85,7 +100,7 @@ export class RolesGuard implements CanActivate {
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;
canAccess = true;
}
if (!teamMembership) {
@@ -98,7 +113,7 @@ export class RolesGuard implements CanActivate {
}
// if user is not admin nor an owner of org, and is part of the team, then check user team membership role
return hasMinimumRole({
canAccess = hasMinimumRole({
checkRole: `TEAM_${teamMembership.role}`,
minimumRole: allowedRole,
roles: TEAM_ROLES,
@@ -106,16 +121,16 @@ export class RolesGuard implements CanActivate {
}
// 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({
else if (ORG_ROLES.includes(allowedRole as unknown as (typeof ORG_ROLES)[number])) {
canAccess = hasMinimumRole({
checkRole: `ORG_${orgMembership.role}`,
minimumRole: allowedRole,
roles: ORG_ROLES,
});
}
}
return false;
await this.redisService.redis.set(REDIS_CACHE_KEY, String(canAccess), "EX", 3600);
return canAccess;
}
}
@@ -9,12 +9,13 @@ import { OrganizationsSchedulesService } from "@/modules/organizations/services/
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, StripeModule, SchedulesModule_2024_06_11, UsersModule],
imports: [PrismaModule, StripeModule, SchedulesModule_2024_06_11, UsersModule, RedisModule],
providers: [
OrganizationsRepository,
OrganizationsTeamsRepository,
+23
View File
@@ -1650,6 +1650,29 @@
]
}
},
"/v2/calendars/{calendar}/credentials": {
"post": {
"operationId": "CalendarsController_syncCredentials",
"parameters": [
{
"name": "calendar",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": ""
}
},
"tags": [
"Calendars"
]
}
},
"/v2/calendars/{calendar}/check": {
"get": {
"operationId": "CalendarsController_check",