Skip to content

Commit

Permalink
feat: add more caching to guards & middlewares
Browse files Browse the repository at this point in the history
  • Loading branch information
evereq committed Mar 5, 2024
1 parent 4044708 commit 25b3f72
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export class TenantSettingsMiddleware implements NestMiddleware {
if (decodedToken && decodedToken.tenantId) {
console.log('Getting Tenant settings from Cache for tenantId: %s', decodedToken.tenantId);

tenantSettings = await this.cacheManager.get('tenantSettings_' + decodedToken.tenantId);
const cacheKey = 'tenantSettings_' + decodedToken.tenantId;

tenantSettings = await this.cacheManager.get(cacheKey);

if (!tenantSettings) {
console.log('Tenant settings NOT loaded from Cache for tenantId: %s', decodedToken.tenantId);
Expand All @@ -47,9 +49,9 @@ export class TenantSettingsMiddleware implements NestMiddleware {

if (tenantSettings) {
await this.cacheManager.set(
'tenantSettings_' + decodedToken.tenantId,
cacheKey,
tenantSettings,
60 * 1000 // 60 seconds caching period for Tenants Settings
5 * 60 * 1000 // 5 min caching period for Tenants Settings
);

console.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ export class IntegrationAIMiddleware implements NestMiddleware {
`Getting Gauzy AI integration settings from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}`
);

const cacheKey = `integrationTenantSettings_${tenantId}_${organizationId}_${IntegrationEnum.GAUZY_AI}`;

// Fetch integration settings from the service
let integrationTenantSettings: IIntegrationSetting[] = await this.cacheManager.get(
`integrationTenantSettings_${tenantId}_${organizationId}_${IntegrationEnum.GAUZY_AI}`
);
let integrationTenantSettings: IIntegrationSetting[] = await this.cacheManager.get(cacheKey);

if (!integrationTenantSettings) {
console.log(
Expand All @@ -65,9 +65,9 @@ export class IntegrationAIMiddleware implements NestMiddleware {
integrationTenantSettings = fromDb.settings;

await this.cacheManager.set(
`integrationTenantSettings_${tenantId}_${organizationId}_${IntegrationEnum.GAUZY_AI}`,
cacheKey,
integrationTenantSettings,
60 * 1000 // 60 seconds caching period for Integration Tenant Settings
5 * 60 * 1000 // 5 min caching period for Integration Tenant Settings
);

console.log(
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/integration/github/github.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export class GithubMiddleware implements NestMiddleware {
`Getting Gauzy integration settings from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}`
);

let integrationTenantSettings: IIntegrationSetting[] = await this.cacheManager.get(
`integrationTenantSettings_${tenantId}_${organizationId}_${integrationId}`
);
const cacheKey = `integrationTenantSettings_${tenantId}_${organizationId}_${integrationId}`;

let integrationTenantSettings: IIntegrationSetting[] = await this.cacheManager.get(cacheKey);

if (!integrationTenantSettings) {
console.log(
Expand All @@ -67,9 +67,9 @@ export class GithubMiddleware implements NestMiddleware {
integrationTenantSettings = fromDb.settings;

await this.cacheManager.set(
`integrationTenantSettings_${tenantId}_${organizationId}_${integrationId}`,
cacheKey,
integrationTenantSettings,
60 * 1000 // 60 seconds caching period for Tenants Settings
5 * 60 * 1000 // 5 min caching period for GitHub Integration Tenant Settings
);

console.log(
Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/role-permission/role-permission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,11 +441,12 @@ export class RolePermissionService extends TenantAwareCrudService<RolePermission
* @returns A Promise with a boolean indicating if the role permissions are valid.
* @throws Error if the ORM type is not implemented.
*/
public async checkRolePermission(permissions: string[], includeRole: boolean = false): Promise<boolean> {
// Retrieve current role ID and tenant ID from RequestContext
const tenantId = RequestContext.currentTenantId();
const roleId = RequestContext.currentRoleId();

public async checkRolePermission(
tenantId: string,
roleId: string,
permissions: string[],
includeRole: boolean = false
): Promise<boolean> {
switch (this.ormType) {
case MultiORMEnum.TypeORM:
// Create a query builder for the 'role_permission' entity
Expand All @@ -454,7 +455,9 @@ export class RolePermissionService extends TenantAwareCrudService<RolePermission
query.where('rp.tenantId = :tenantId', { tenantId });

// If includeRole is true, add the condition for the current role ID
if (includeRole) { query.andWhere('rp.roleId = :roleId', { roleId }); }
if (includeRole) {
query.andWhere('rp.roleId = :roleId', { roleId });
}

// Add conditions for permissions, enabled, isActive, and isArchived
query.andWhere('rp.permission IN (:...permissions)', { permissions });
Expand Down
26 changes: 23 additions & 3 deletions packages/core/src/shared/guards/feature-flag.guard.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { CanActivate, ExecutionContext, Injectable, NotFoundException, Type } from '@nestjs/common';
import { CanActivate, ExecutionContext, Inject, Injectable, NotFoundException, Type } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { FEATURE_METADATA } from '@gauzy/common';
import { FeatureEnum } from '@gauzy/contracts';
import { FeatureService } from './../../feature/feature.service';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/cache-manager';

/**
* Feature enabled/disabled guard
Expand All @@ -11,7 +13,11 @@ import { FeatureService } from './../../feature/feature.service';
*/
@Injectable()
export class FeatureFlagGuard implements CanActivate {
constructor(private readonly _reflector: Reflector, private readonly featureFlagService: FeatureService) {}
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
private readonly _reflector: Reflector,
private readonly featureFlagService: FeatureService
) {}

/**
* Determines if the current request can be activated based on feature flag metadata.
Expand All @@ -30,8 +36,22 @@ export class FeatureFlagGuard implements CanActivate {

console.log('Guard: FeatureFlag checking', flag);

const cacheKey = `featureFlag_${flag}`;

const fromCache = await this.cacheManager.get<boolean | null>(cacheKey);

let isEnabled: boolean;

if (fromCache == null) {
isEnabled = await this.featureFlagService.isFeatureEnabled(flag);

await this.cacheManager.set(cacheKey, isEnabled);
} else {
isEnabled = fromCache;
}

// Check if the feature is enabled
if (await this.featureFlagService.isFeatureEnabled(flag)) {
if (isEnabled) {
console.log(`Guard: FeatureFlag ${flag} enabled`);
return true;
}
Expand Down
57 changes: 47 additions & 10 deletions packages/core/src/shared/guards/organization-permission.guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { environment as env } from '@gauzy/config';
import { CanActivate, ExecutionContext, Injectable, Type } from '@nestjs/common';
import { CanActivate, ExecutionContext, Inject, Injectable, Type } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { verify } from 'jsonwebtoken';
import { PermissionsEnum, RolesEnum } from '@gauzy/contracts';
Expand All @@ -8,14 +8,16 @@ import * as camelCase from 'camelcase';
import { RequestContext } from './../../core/context';
import { Brackets, WhereExpressionBuilder } from 'typeorm';
import { TypeOrmEmployeeRepository } from '../../employee/repository/type-orm-employee.repository';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/cache-manager';

@Injectable()
export class OrganizationPermissionGuard implements CanActivate {

constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
readonly _reflector: Reflector,
readonly _typeOrmEmployeeRepository: TypeOrmEmployeeRepository
) { }
) {}

/**
* Checks if the user is authorized based on specified permissions.
Expand All @@ -25,7 +27,9 @@ export class OrganizationPermissionGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
// Retrieve permissions from metadata
const targets: Array<Function | Type<any>> = [context.getHandler(), context.getClass()];
const permissions = removeDuplicates(this._reflector.getAllAndOverride<PermissionsEnum[]>(PERMISSIONS_METADATA, targets)) || [];

const permissions =
removeDuplicates(this._reflector.getAllAndOverride<PermissionsEnum[]>(PERMISSIONS_METADATA, targets)) || [];

// If no specific permissions are required, consider it authorized
if (isEmpty(permissions)) {
Expand All @@ -50,17 +54,51 @@ export class OrganizationPermissionGuard implements CanActivate {

// Check permissions based on user role
if (role === RolesEnum.EMPLOYEE) {
console.log(`Guard: Organization Permissions for Employee ID: ${employeeId}`);
// Check if user has the required permissions
isAuthorized = await this.checkOrganizationPermission(employeeId, permissions);
const tenantId = RequestContext.currentTenantId();

const cacheKey = `orgPermissions_${tenantId}_${employeeId}_${permissions.join('_')}`;

console.log(
`Guard: Checking Org Permissions for Employee ID: ${employeeId} from Cache with key ${cacheKey}`
);

const fromCache = await this.cacheManager.get<boolean | null>(cacheKey);

if (fromCache == null) {
console.log('Organization Permissions NOT loaded from Cache with key:', cacheKey);

// Check if user has the required permissions
isAuthorized = await this.checkOrganizationPermission(tenantId, employeeId, permissions);

await this.cacheManager.set(
cacheKey,
isAuthorized,
5 * 60 * 1000 // 5 minutes caching period for Organization Permissions
);
} else {
isAuthorized = fromCache;
console.log(`Organization Permissions loaded from Cache with key: ${cacheKey}. Value: ${isAuthorized}`);
}
} else {
// For non-employee roles, consider it authorized
// TODO: why!? This should be handled differently I think...
// If it's not Employee, but say Viewer, it should still check the permissions...
isAuthorized = true;
}

if (!isAuthorized) {
// Log unauthorized access attempts
console.log(`Unauthorized access blocked: User ID: ${id}, Role: ${role}, Employee ID: ${employeeId}, Permissions Checked: ${permissions.join(', ')}`);
console.log(
`Unauthorized access blocked: User ID: ${id}, Role: ${role}, Employee ID: ${employeeId}, Permissions Checked: ${permissions.join(
', '
)}`
);
} else {
console.log(
`Access granted. User ID: ${id}, Role: ${role}, Employee ID: ${employeeId}, Permissions Checked: ${permissions.join(
', '
)}`
);
}

return isAuthorized;
Expand All @@ -72,10 +110,9 @@ export class OrganizationPermissionGuard implements CanActivate {
* @param permissions - An array of permission strings to check.
* @returns A Promise that resolves to a boolean indicating if at least one permission is allowed in the organization.
*/
async checkOrganizationPermission(employeeId: string, permissions: string[]): Promise<boolean> {
async checkOrganizationPermission(tenantId: string, employeeId: string, permissions: string[]): Promise<boolean> {
try {
console.time('Organization Permission Guard Time');
const tenantId = RequestContext.currentTenantId();
// Create a query builder for the 'employee' entity
const query = this._typeOrmEmployeeRepository.createQueryBuilder('employee');
// Inner join with the 'organization' entity
Expand Down
51 changes: 45 additions & 6 deletions packages/core/src/shared/guards/permission.guard.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { environment as env } from '@gauzy/config';
import { CanActivate, ExecutionContext, Injectable, Type } from '@nestjs/common';
import { CanActivate, ExecutionContext, Inject, Injectable, Type } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { verify } from 'jsonwebtoken';
import { isEmpty, PERMISSIONS_METADATA, removeDuplicates } from '@gauzy/common';
import { PermissionsEnum } from '@gauzy/contracts';
import { RequestContext } from './../../core/context';
import { RolePermissionService } from '../../role-permission/role-permission.service';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/cache-manager';

@Injectable()
export class PermissionGuard implements CanActivate {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
private readonly _reflector: Reflector,
private readonly _rolePermissionService: RolePermissionService
) { }
) {}

/**
* Checks if the user is authorized based on specified permissions.
Expand All @@ -22,7 +25,8 @@ export class PermissionGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
// Retrieve permissions from metadata
const targets: Array<Function | Type<any>> = [context.getHandler(), context.getClass()];
const permissions = removeDuplicates(this._reflector.getAllAndOverride<PermissionsEnum[]>(PERMISSIONS_METADATA, targets)) || [];
const permissions =
removeDuplicates(this._reflector.getAllAndOverride<PermissionsEnum[]>(PERMISSIONS_METADATA, targets)) || [];

// If no specific permissions are required, consider it authorized
if (isEmpty(permissions)) {
Expand All @@ -33,13 +37,48 @@ export class PermissionGuard implements CanActivate {
const token = RequestContext.currentToken();
const { id, role } = verify(token, env.JWT_SECRET) as { id: string; role: string };

// Check if user has the required permissions
const isAuthorized = await this._rolePermissionService.checkRolePermission(permissions, true);
// Retrieve current role ID and tenant ID from RequestContext
const tenantId = RequestContext.currentTenantId();
const roleId = RequestContext.currentRoleId();

const cacheKey = `userPermissions_${tenantId}_${roleId}_${permissions.join('_')}`;

console.log('Checking User Permissions from Cache with key:', cacheKey);

let isAuthorized = false;

const fromCache = await this.cacheManager.get<boolean | null>(cacheKey);

if (fromCache == null) {
console.log('User Permissions NOT loaded from Cache with key:', cacheKey);

// Check if user has the required permissions
isAuthorized = await this._rolePermissionService.checkRolePermission(tenantId, roleId, permissions, true);

await this.cacheManager.set(
cacheKey,
isAuthorized,
5 * 60 * 1000 // 5 minutes cache expiration time for User Permissions
);
} else {
isAuthorized = fromCache;
console.log(`User Permissions loaded from Cache with key: ${cacheKey}. Value: ${isAuthorized}`);
}

// Log unauthorized access attempts
if (!isAuthorized) {
// Log unauthorized access attempts
console.log(`Unauthorized access blocked: User ID: ${id}, Role: ${role}, Permissions Checked: ${permissions.join(', ')}`);
console.log(
`Unauthorized access blocked: User ID: ${id}, Role: ${role}, Tenant ID:', ${tenantId}, Permissions Checked: ${permissions.join(
', '
)}`
);
} else {
console.log(
`Access granted. User ID: ${id}, Role: ${role}, Tenant ID:', ${tenantId}, Permissions Checked: ${permissions.join(
', '
)}`
);
}

return isAuthorized;
Expand Down
Loading

0 comments on commit 25b3f72

Please sign in to comment.