chore: add caching apiv2 roles guard (#15694)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user