From 3f9d1c1abf23126e8743f68ad13a2b9fd1bfd590 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:39:06 +0300 Subject: [PATCH] chore: add caching apiv2 roles guard (#15694) --- .../modules/auth/guards/roles/roles.guard.ts | 49 ++++++++++++------- .../organizations/organizations.module.ts | 3 +- apps/api/v2/swagger/documentation.json | 23 +++++++++ 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts b/apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts index 7a950b21ca..976c50faab 100644 --- a/apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts @@ -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 { const request = context.switchToHttp().getRequest(); @@ -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; } } diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 4368cb1a49..0ea1014ba1 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -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, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 1deb21b26f..8550aa638d 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -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",