guard level fixes (#13153)

This commit is contained in:
Midhun G S 2025-07-02 16:16:24 +05:30 committed by GitHub
parent abae4dc009
commit c38c12327f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 75 additions and 75 deletions

View file

@ -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;
}
}

View file

@ -19,6 +19,7 @@ export interface FeatureConfig {
auditLogsKey?: string;
skipAuditLogs?: boolean;
isPublic?: boolean;
isSuperAdminFeature?: boolean;
shouldNotSkipPublicApp?: boolean;
}

View file

@ -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;
}

View file

@ -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<boolean> {
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);

View file

@ -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<boolean> {
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);

View file

@ -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<boolean> {
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_FIELD>(LICENSE_FEATURE_ID_KEY, context.getHandler()) ||
const licenseFeatureId = (this.reflector.get<LICENSE_FIELD>('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

View file

@ -14,12 +14,10 @@ export class UserCountGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
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

View file

@ -275,30 +275,6 @@ export class LicenseCountsService implements ILicenseCountsService {
);
}
async getUserIdWithEndUserRole(manager: EntityManager): Promise<string[]> {
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<number> {
if (getTooljetEdition() !== TOOLJET_EDITIONS.Cloud) {
// If the edition is cloud, we do not filter by organizationId

View file

@ -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);

View file

@ -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<any> {
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;
}
}

View file

@ -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<any> {
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;
}
}

View file

@ -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]: {},

View file

@ -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',
},
},