From c38c12327f7e8b2d0b91c401f03a5cbee79b415d Mon Sep 17 00:00:00 2001 From: Midhun G S Date: Wed, 2 Jul 2025 16:16:24 +0530 Subject: [PATCH] guard level fixes (#13153) --- .../src/modules/app/guards/ability.guard.ts | 21 ++++++++++++---- server/src/modules/app/types.ts | 1 + .../auth/guards/authorize-workspace-guard.ts | 1 + .../src/modules/licensing/guards/app.guard.ts | 8 +++---- .../licensing/guards/editorUser.guard.ts | 22 ++++++++++------- .../modules/licensing/guards/feature.guard.ts | 17 ++++++------- .../modules/licensing/guards/user.guard.ts | 10 ++++---- .../licensing/services/count.service.ts | 24 ------------------- server/src/modules/onboarding/controller.ts | 9 +------ .../guards/first-user-signup-disable.guard.ts | 9 ++++++- .../guards/first-user-signup.guard.ts | 9 ++++++- .../organization-users/constants/feature.ts | 7 ++---- .../src/modules/users/constants/features.ts | 12 ++++++---- 13 files changed, 75 insertions(+), 75 deletions(-) diff --git a/server/src/modules/app/guards/ability.guard.ts b/server/src/modules/app/guards/ability.guard.ts index e97f64c149..233f0d1e28 100644 --- a/server/src/modules/app/guards/ability.guard.ts +++ b/server/src/modules/app/guards/ability.guard.ts @@ -7,6 +7,7 @@ import { LicenseTermsService } from '@modules/licensing/interfaces/IService'; import { FeatureConfig, ResourceDetails } from '../types'; import { App } from '@entities/app.entity'; import { MODULES } from '../constants/modules'; +import { isSuperAdmin } from '@helpers/utils.helper'; // User should be present or app should be public @Injectable() @@ -42,7 +43,6 @@ export abstract class AbilityGuard implements CanActivate { } const request = context.switchToHttp().getRequest(); - const orgId = request.headers['tj-workspace-id']; const user = request.user; const app: App = request.tj_app; if (app) { @@ -61,9 +61,13 @@ export abstract class AbilityGuard implements CanActivate { } const licenseRequired: LICENSE_FIELD = featureInfo?.license; + if (licenseRequired && !(app?.organizationId || user?.organizationId)) { + // If no license is required, continue to the next feature + continue; + } if ( licenseRequired && - !(await this.licenseTermsService.getLicenseTerms(licenseRequired, app?.organizationId || orgId)) + !(await this.licenseTermsService.getLicenseTerms(licenseRequired, app?.organizationId || user?.organizationId)) ) { throw new HttpException( `Oops! Your current plan doesn't have access to this feature. Please upgrade your plan now to use this.`, @@ -73,12 +77,21 @@ export abstract class AbilityGuard implements CanActivate { // If any of the feature is public if (featureInfo.isPublic) { - return true; + // No other validations if user is API is public + continue; + } + + if (featureInfo.isSuperAdminFeature && !isSuperAdmin(user)) { + // If the user is not super admin and the feature is a super admin feature + throw new ForbiddenException({ + message: 'You do not have permission to access this resource', + organizationId: app?.organizationId, + }); } if (app?.isPublic && !featureInfo.shouldNotSkipPublicApp) { // No need to do validations if app is public - return true; + continue; } } diff --git a/server/src/modules/app/types.ts b/server/src/modules/app/types.ts index 16ee1ce0eb..68f6185976 100644 --- a/server/src/modules/app/types.ts +++ b/server/src/modules/app/types.ts @@ -19,6 +19,7 @@ export interface FeatureConfig { auditLogsKey?: string; skipAuditLogs?: boolean; isPublic?: boolean; + isSuperAdminFeature?: boolean; shouldNotSkipPublicApp?: boolean; } diff --git a/server/src/modules/auth/guards/authorize-workspace-guard.ts b/server/src/modules/auth/guards/authorize-workspace-guard.ts index 26d7b19f9e..d25e459e4f 100644 --- a/server/src/modules/auth/guards/authorize-workspace-guard.ts +++ b/server/src/modules/auth/guards/authorize-workspace-guard.ts @@ -41,6 +41,7 @@ export class AuthorizeWorkspaceGuard extends AuthGuard('jwt') { try { user = super.canActivate(context); + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (err) { return false; } diff --git a/server/src/modules/licensing/guards/app.guard.ts b/server/src/modules/licensing/guards/app.guard.ts index 8a11b8d7a4..3758ce9367 100644 --- a/server/src/modules/licensing/guards/app.guard.ts +++ b/server/src/modules/licensing/guards/app.guard.ts @@ -7,6 +7,7 @@ import { App } from '@entities/app.entity'; import { APP_TYPES } from '@modules/apps/constants'; import { getTooljetEdition } from '@helpers/utils.helper'; import { TOOLJET_EDITIONS } from '@modules/app/constants'; +import { WORKSPACE_STATUS } from '@modules/users/constants/lifecycle'; @Injectable() export class AppCountGuard implements CanActivate { @@ -18,7 +19,7 @@ export class AppCountGuard implements CanActivate { const whereCondition: any = { type: APP_TYPES.FRONT_END, organization: { - status: 'active', + status: WORKSPACE_STATUS.ACTIVE, }, }; // Fetch apps using organization ID only for cloud @@ -38,10 +39,7 @@ export class AppCountGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const organizationId = - typeof request.headers['tj-workspace-id'] === 'object' - ? request.headers['tj-workspace-id'][0] - : request.headers['tj-workspace-id']; + const organizationId = request?.user?.organizationId; return await dbTransactionWrap(async (manager: EntityManager) => { const appCount = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.APP_COUNT, organizationId); diff --git a/server/src/modules/licensing/guards/editorUser.guard.ts b/server/src/modules/licensing/guards/editorUser.guard.ts index 37f3115403..4a7c232952 100644 --- a/server/src/modules/licensing/guards/editorUser.guard.ts +++ b/server/src/modules/licensing/guards/editorUser.guard.ts @@ -1,10 +1,12 @@ import { Injectable, CanActivate, ExecutionContext, HttpException } from '@nestjs/common'; import { LicenseCountsService } from '@modules/licensing/services/count.service'; -import { LICENSE_FIELD, LICENSE_LIMIT } from '@modules/licensing/constants'; +import { LICENSE_FIELD, LICENSE_LIMIT, ORGANIZATION_INSTANCE_KEY } from '@modules/licensing/constants'; import { DataSource } from 'typeorm'; import { LicenseTermsService } from '../interfaces/IService'; import { InstanceSettingsUtilService } from '@modules/instance-settings/util.service'; import { INSTANCE_USER_SETTINGS } from '@modules/instance-settings/constants'; +import { getTooljetEdition } from '@helpers/utils.helper'; +import { TOOLJET_EDITIONS } from '@modules/app/constants'; @Injectable() export class EditorUserCountGuard implements CanActivate { @@ -17,20 +19,24 @@ export class EditorUserCountGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const organizationId = - typeof request.headers['tj-workspace-id'] === 'object' - ? request.headers['tj-workspace-id'][0] - : request.headers['tj-workspace-id']; - const isWorkspaceSignup = !!request.body.organizationId; + const organizationId = request.body.organizationId; + const isWorkspaceSignup = !!organizationId; + if (!isWorkspaceSignup && getTooljetEdition() === TOOLJET_EDITIONS.Cloud) { + // Not needed for cloud edition, as it is not used in the cloud + return true; + } const isPersonalWorkspaceEnabled = (await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) === 'true'; if (isWorkspaceSignup && !isPersonalWorkspaceEnabled) return true; - const editorsCount = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.EDITORS, organizationId); + const editorsCount = await this.licenseTermsService.getLicenseTermsInstance(LICENSE_FIELD.EDITORS); if (editorsCount === LICENSE_LIMIT.UNLIMITED) { return true; } - const editorCount = await this.licenseCountsService.fetchTotalEditorCount(organizationId, this._dataSource.manager); + const editorCount = await this.licenseCountsService.fetchTotalEditorCount( + organizationId || ORGANIZATION_INSTANCE_KEY, + this._dataSource.manager + ); if (editorCount >= editorsCount) { throw new HttpException('Maximum editor user limit reached', 451); diff --git a/server/src/modules/licensing/guards/feature.guard.ts b/server/src/modules/licensing/guards/feature.guard.ts index 578818375c..de946fe7be 100644 --- a/server/src/modules/licensing/guards/feature.guard.ts +++ b/server/src/modules/licensing/guards/feature.guard.ts @@ -2,11 +2,13 @@ import { Injectable, CanActivate, ExecutionContext, HttpException } from '@nestj import { Reflector } from '@nestjs/core'; import { LICENSE_FIELD } from '../constants'; import { LicenseTermsService } from '../interfaces/IService'; -import { LICENSE_FEATURE_ID_KEY } from '@modules/app/constants'; @Injectable() export class FeatureGuard implements CanActivate { - constructor(protected reflector: Reflector, protected licenseTermsService: LicenseTermsService) {} + constructor( + protected reflector: Reflector, + protected licenseTermsService: LicenseTermsService + ) {} protected currentFeatureId: string; @@ -17,14 +19,13 @@ export class FeatureGuard implements CanActivate { } async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const organizationId = - typeof request.headers['tj-workspace-id'] === 'object' - ? request.headers['tj-workspace-id'][0] - : request.headers['tj-workspace-id']; - const licenseFeatureId = (this.reflector.get(LICENSE_FEATURE_ID_KEY, context.getHandler()) || + const licenseFeatureId = (this.reflector.get('tjLicenseFeatureId', context.getHandler()) || this.currentFeatureId) as LICENSE_FIELD; - if (!licenseFeatureId || !(await this.licenseTermsService.getLicenseTerms(licenseFeatureId, organizationId))) { + if ( + !licenseFeatureId || + !(await this.licenseTermsService.getLicenseTerms(licenseFeatureId, request?.user?.organizationId)) + ) { throw new HttpException( `Oops! Your current plan doesn't have access to this feature. Please upgrade your plan now to use this.`, 451 diff --git a/server/src/modules/licensing/guards/user.guard.ts b/server/src/modules/licensing/guards/user.guard.ts index 7806292e7c..0ae7b82039 100644 --- a/server/src/modules/licensing/guards/user.guard.ts +++ b/server/src/modules/licensing/guards/user.guard.ts @@ -14,12 +14,10 @@ export class UserCountGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const organizationId = - typeof request.headers['tj-workspace-id'] === 'object' - ? request.headers['tj-workspace-id'][0] - : request.headers['tj-workspace-id']; - const totalUsers = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.TOTAL_USERS, organizationId); - + const organizationId = request.body.organizationId; + const totalUsers = organizationId + ? await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.TOTAL_USERS, organizationId) + : await this.licenseTermsService.getLicenseTermsInstance(LICENSE_FIELD.TOTAL_USERS); if ( totalUsers !== LICENSE_LIMIT.UNLIMITED && (await this.licenseCountsService.getUsersCount(organizationId)) >= totalUsers diff --git a/server/src/modules/licensing/services/count.service.ts b/server/src/modules/licensing/services/count.service.ts index 2923bba6b7..e807a38393 100644 --- a/server/src/modules/licensing/services/count.service.ts +++ b/server/src/modules/licensing/services/count.service.ts @@ -275,30 +275,6 @@ export class LicenseCountsService implements ILicenseCountsService { ); } - async getUserIdWithEndUserRole(manager: EntityManager): Promise { - const statusList = [WORKSPACE_USER_STATUS.INVITED, WORKSPACE_USER_STATUS.ACTIVE]; - - const users = await manager.find(User, { - select: ['id'], - where: { - status: Not(USER_STATUS.ARCHIVED), - organizationUsers: { - status: In(statusList), - }, - userPermissions: { - name: USER_ROLE.END_USER, - organization: { - status: WORKSPACE_STATUS.ACTIVE, - }, - }, - }, - relations: ['organizationUsers', 'userPermissions', 'userPermissions.organization'], - }); - - // Extract unique user IDs - return [...new Set(users.map((user) => user.id))]; - } - async fetchTotalAppCount(organizationId: string, manager: EntityManager): Promise { if (getTooljetEdition() !== TOOLJET_EDITIONS.Cloud) { // If the edition is cloud, we do not filter by organizationId diff --git a/server/src/modules/onboarding/controller.ts b/server/src/modules/onboarding/controller.ts index a523e0e029..76a45b4d2b 100644 --- a/server/src/modules/onboarding/controller.ts +++ b/server/src/modules/onboarding/controller.ts @@ -15,7 +15,6 @@ import { CreateAdminDto, OnboardUserDto } from '@modules/onboarding/dto/user.dto import { AcceptInviteDto } from './dto/accept-organization-invite.dto'; import { AppSignupDto } from '@modules/auth/dto'; import { SignupDisableGuard } from './guards/signup-disable.guard'; -import { AllowPersonalWorkspaceGuard } from './guards/personal-workspace.guard'; import { FirstUserSignupGuard } from './guards/first-user-signup.guard'; import { UserCountGuard } from '@modules/licensing/guards/user.guard'; import { EditorUserCountGuard } from '@modules/licensing/guards/editorUser.guard'; @@ -47,13 +46,7 @@ export class OnboardingController implements IOnboardingController { } @InitFeature(FEATURE_KEY.SIGNUP) - @UseGuards( - SignupDisableGuard, - UserCountGuard, - EditorUserCountGuard, - FirstUserSignupDisableGuard, - FeatureAbilityGuard - ) + @UseGuards(SignupDisableGuard, UserCountGuard, EditorUserCountGuard, FirstUserSignupDisableGuard, FeatureAbilityGuard) @Post('signup') async signup(@Body() appSignupDto: AppSignupDto) { return this.onboardingService.signup(appSignupDto); diff --git a/server/src/modules/onboarding/guards/first-user-signup-disable.guard.ts b/server/src/modules/onboarding/guards/first-user-signup-disable.guard.ts index 87d8098cdf..b30e29b488 100644 --- a/server/src/modules/onboarding/guards/first-user-signup-disable.guard.ts +++ b/server/src/modules/onboarding/guards/first-user-signup-disable.guard.ts @@ -1,10 +1,17 @@ import { Injectable, CanActivate } from '@nestjs/common'; import { LicenseCountsService } from '@modules/licensing/services/count.service'; +import { getTooljetEdition } from '@helpers/utils.helper'; +import { TOOLJET_EDITIONS } from '@modules/app/constants'; +import { ORGANIZATION_INSTANCE_KEY } from '@modules/licensing/constants'; @Injectable() export class FirstUserSignupDisableGuard implements CanActivate { constructor(protected readonly licenseCountsService: LicenseCountsService) {} async canActivate(): Promise { - return (await this.licenseCountsService.getUsersCount('INSTANCE')) !== 0; + if (getTooljetEdition() === TOOLJET_EDITIONS.Cloud) { + // Not needed for cloud edition, as it is not used in the cloud + return true; + } + return (await this.licenseCountsService.getUsersCount(ORGANIZATION_INSTANCE_KEY)) !== 0; } } diff --git a/server/src/modules/onboarding/guards/first-user-signup.guard.ts b/server/src/modules/onboarding/guards/first-user-signup.guard.ts index 43807dea76..3ea2c781d4 100644 --- a/server/src/modules/onboarding/guards/first-user-signup.guard.ts +++ b/server/src/modules/onboarding/guards/first-user-signup.guard.ts @@ -1,3 +1,6 @@ +import { getTooljetEdition } from '@helpers/utils.helper'; +import { TOOLJET_EDITIONS } from '@modules/app/constants'; +import { ORGANIZATION_INSTANCE_KEY } from '@modules/licensing/constants'; import { LicenseCountsService } from '@modules/licensing/services/count.service'; import { Injectable, CanActivate } from '@nestjs/common'; @Injectable() @@ -5,6 +8,10 @@ export class FirstUserSignupGuard implements CanActivate { constructor(protected readonly licenseCountsService: LicenseCountsService) {} async canActivate(): Promise { - return (await this.licenseCountsService.getUsersCount('INSTANCE')) === 0; + if (getTooljetEdition() === TOOLJET_EDITIONS.Cloud) { + // Not needed for cloud edition, as it is not used in the cloud + return false; + } + return (await this.licenseCountsService.getUsersCount(ORGANIZATION_INSTANCE_KEY)) === 0; } } diff --git a/server/src/modules/organization-users/constants/feature.ts b/server/src/modules/organization-users/constants/feature.ts index 27b9eb8f7e..3e9ed37ede 100644 --- a/server/src/modules/organization-users/constants/feature.ts +++ b/server/src/modules/organization-users/constants/feature.ts @@ -7,24 +7,21 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.SUGGEST_USERS]: {}, [FEATURE_KEY.VIEW_ALL_USERS]: {}, [FEATURE_KEY.USER_ARCHIVE_ALL]: { - isPublic: true, + isSuperAdminFeature: true, auditLogsKey: 'USER_ARCHIVE', }, [FEATURE_KEY.USER_ARCHIVE]: { - isPublic: true, auditLogsKey: 'USER_ARCHIVE', }, [FEATURE_KEY.USER_INVITE]: { - isPublic: true, auditLogsKey: 'USER_INVITE', }, [FEATURE_KEY.USER_BULK_UPLOAD]: {}, [FEATURE_KEY.USER_UNARCHIVE]: { - isPublic: true, auditLogsKey: 'USER_UNARCHIVE', }, [FEATURE_KEY.USER_UNARCHIVE_ALL]: { - isPublic: true, + isSuperAdminFeature: true, auditLogsKey: 'USER_UNARCHIVE', }, [FEATURE_KEY.USER_UPDATE]: {}, diff --git a/server/src/modules/users/constants/features.ts b/server/src/modules/users/constants/features.ts index 1524e517a1..51bdf59e3f 100644 --- a/server/src/modules/users/constants/features.ts +++ b/server/src/modules/users/constants/features.ts @@ -4,21 +4,23 @@ import { FeaturesConfig } from '../types'; export const FEATURES: FeaturesConfig = { [MODULES.USER]: { - [FEATURE_KEY.GET_ALL_USERS]: {}, + [FEATURE_KEY.GET_ALL_USERS]: { + isSuperAdminFeature: true, + }, [FEATURE_KEY.UPDATE_USER_TYPE]: { - isPublic: true, + isSuperAdminFeature: true, auditLogsKey: 'USER_DETAILS_UPDATE', }, [FEATURE_KEY.UPDATE_USER_TYPE_INSTANCE]: { - isPublic: true, + isSuperAdminFeature: true, auditLogsKey: 'SET_AS_SUPERADMIN', }, [FEATURE_KEY.AUTO_UPDATE_USER_PASSWORD]: { - isPublic: true, + isSuperAdminFeature: true, auditLogsKey: 'USER_PASSWORD_RESET', }, [FEATURE_KEY.CHANGE_USER_PASSWORD]: { - isPublic: true, + isSuperAdminFeature: true, auditLogsKey: 'USER_PASSWORD_RESET', }, },