mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 00:48:25 +00:00
guard level fixes (#13153)
This commit is contained in:
parent
abae4dc009
commit
c38c12327f
13 changed files with 75 additions and 75 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface FeatureConfig {
|
|||
auditLogsKey?: string;
|
||||
skipAuditLogs?: boolean;
|
||||
isPublic?: boolean;
|
||||
isSuperAdminFeature?: boolean;
|
||||
shouldNotSkipPublicApp?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]: {},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue