From 0ee15a99e2e7ec2652f35b0a59f5905d4fb70119 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 20 Feb 2026 12:32:21 +0000 Subject: [PATCH] refactor --- .../data-structures/found-connections.ds.ts | 7 +- .../application/dto/create-connection.dto.ts | 14 +-- ....ts => updated-connection-response.dto.ts} | 0 .../connection/connection.controller.ts | 88 +++---------------- .../entities/connection/connection.entity.ts | 1 - .../entities/connection/connection.module.ts | 1 - .../custom-connection-repository-extension.ts | 7 +- .../ssl-certificate/read-certificate.ts | 8 +- ...ssions-for-group-in-connection.use.case.ts | 26 ++++-- .../use-cases/test-connection.use.case.ts | 11 +-- .../use-cases/update-connection.use.case.ts | 23 ++--- .../connection/utils/is-host-allowed.ts | 37 +++++--- .../utils/validate-create-connection-data.ts | 84 +++++------------- backend/src/exceptions/text/messages.ts | 2 +- 14 files changed, 114 insertions(+), 195 deletions(-) rename backend/src/entities/connection/application/dto/{updated-connection-responce.dto.ts => updated-connection-response.dto.ts} (100%) diff --git a/backend/src/entities/connection/application/data-structures/found-connections.ds.ts b/backend/src/entities/connection/application/data-structures/found-connections.ds.ts index dcb072d0b..d8462df66 100644 --- a/backend/src/entities/connection/application/data-structures/found-connections.ds.ts +++ b/backend/src/entities/connection/application/data-structures/found-connections.ds.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; import { AccessLevelEnum } from '../../../../enums/index.js'; +import { ConnectionPropertiesEntity } from '../../../connection-properties/connection-properties.entity.js'; import { FoundGroupDataWithUsersDs } from '../../../group/application/data-sctructures/found-user-groups.ds.js'; import { SimpleFoundUserInfoDs } from '../../../user/dto/found-user.dto.js'; import { UserEntity } from '../../../user/user.entity.js'; @@ -79,7 +80,7 @@ export class FoundDirectConnectionsDs { isTestConnection: boolean; @ApiProperty({ required: false }) - connection_properties?: any; + connection_properties?: ConnectionPropertiesEntity; @ApiProperty() isFrozen: boolean; @@ -102,7 +103,7 @@ export class FoundDirectConnectionsNonePermissionDs { isTestConnection: boolean; @ApiProperty() - connection_properties: any; + connection_properties: ConnectionPropertiesEntity; @ApiProperty() isFrozen: boolean; @@ -131,7 +132,7 @@ export class FoundAgentConnectionsDs { isTestConnection: boolean; @ApiProperty({ required: false }) - connection_properties: any; + connection_properties: ConnectionPropertiesEntity; } export class FoundSipleConnectionInfoDS { diff --git a/backend/src/entities/connection/application/dto/create-connection.dto.ts b/backend/src/entities/connection/application/dto/create-connection.dto.ts index fb9f7686d..1a1023834 100644 --- a/backend/src/entities/connection/application/dto/create-connection.dto.ts +++ b/backend/src/entities/connection/application/dto/create-connection.dto.ts @@ -3,7 +3,7 @@ import { ConnectionTypesEnum, ConnectionTypeTestEnum, } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; -import { IsBoolean, IsEnum, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'; +import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min, ValidateIf } from 'class-validator'; import { isTest } from '../../../../helpers/app/is-test.js'; export class CreateConnectionDto { @@ -63,24 +63,28 @@ export class CreateConnectionDto { @ApiProperty({ required: false }) ssh?: boolean; - @IsOptional() + @ValidateIf((o) => o.ssh === true) + @IsNotEmpty({ message: 'SSH private key is required when SSH is enabled' }) @IsString() @ApiProperty({ required: false }) privateSSHKey?: string; - @IsOptional() + @ValidateIf((o) => o.ssh === true) + @IsNotEmpty({ message: 'SSH host is required when SSH is enabled' }) @IsString() @ApiProperty({ required: false }) sshHost?: string; - @IsOptional() + @ValidateIf((o) => o.ssh === true) + @IsNotEmpty({ message: 'SSH port is required when SSH is enabled' }) @IsNumber({ maxDecimalPlaces: 0 }) @Max(65535) @Min(0) @ApiProperty({ required: false }) sshPort?: number; - @IsOptional() + @ValidateIf((o) => o.ssh === true) + @IsNotEmpty({ message: 'SSH username is required when SSH is enabled' }) @IsString() @ApiProperty({ required: false }) sshUsername?: string; diff --git a/backend/src/entities/connection/application/dto/updated-connection-responce.dto.ts b/backend/src/entities/connection/application/dto/updated-connection-response.dto.ts similarity index 100% rename from backend/src/entities/connection/application/dto/updated-connection-responce.dto.ts rename to backend/src/entities/connection/application/dto/updated-connection-response.dto.ts diff --git a/backend/src/entities/connection/connection.controller.ts b/backend/src/entities/connection/connection.controller.ts index d3d351e57..a3e57631a 100644 --- a/backend/src/entities/connection/connection.controller.ts +++ b/backend/src/entities/connection/connection.controller.ts @@ -4,7 +4,6 @@ import { Controller, Get, Inject, - Injectable, Post, Put, Query, @@ -15,8 +14,6 @@ import { ApiBearerAuth, ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } import { SkipThrottle } from '@nestjs/throttler'; import { isRedisConnectionUrl } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; import { TestConnectionResultDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/test-result-connection.ds.js'; -import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; -import validator from 'validator'; import { IGlobalDatabaseContext } from '../../common/application/global-database-context.interface.js'; import { BaseType, UseCaseType } from '../../common/data-injection.tokens.js'; import { BodyUuid, GCLlId, MasterPassword, QueryUuid, SlugUuid, UserId } from '../../decorators/index.js'; @@ -26,7 +23,6 @@ import { Messages } from '../../exceptions/text/messages.js'; import { processExceptionMessage } from '../../exceptions/utils/process-exception-message.js'; import { ConnectionEditGuard, ConnectionReadGuard } from '../../guards/index.js'; import { - isConnectionEntityAgent, isConnectionTypeAgent, slackPostMessage, toPrettyErrorsMsg, @@ -61,7 +57,7 @@ import { FoundUserGroupsInConnectionDTO } from './application/dto/found-user-gro import { ConnectionTokenResponseDTO } from './application/dto/new-connection-token-response.dto.js'; import { TestConnectionResponseDTO } from './application/dto/test-connection-response.dto.js'; import { UpdateMasterPasswordRequestBodyDto } from './application/dto/update-master-password-request-body.dto.js'; -import { UpdatedConnectionResponseDTO } from './application/dto/updated-connection-responce.dto.js'; +import { UpdatedConnectionResponseDTO } from './application/dto/updated-connection-response.dto.js'; import { ValidationResultRo } from './application/dto/validation-result.ro.js'; import { ICreateConnection, @@ -84,13 +80,13 @@ import { } from './use-cases/use-cases.interfaces.js'; import { TokenValidationResult } from './use-cases/validate-connection-token.use.case.js'; import { isTestConnectionUtil } from './utils/is-test-connection-util.js'; +import { validateCreateConnectionData } from './utils/validate-create-connection-data.js'; @UseInterceptors(SentryInterceptor) @Timeout() @Controller() @ApiBearerAuth() @ApiTags('Connection') -@Injectable() export class ConnectionController { constructor( @Inject(UseCaseType.FIND_CONNECTIONS) @@ -141,7 +137,6 @@ export class ConnectionController { }) @Get('/connections') async findAll(@UserId() userId: string, @GCLlId() glidCookieValue: string): Promise { - console.log(`findAll triggered in connection.controller ->: ${new Date().toISOString()}`); const userData: FindUserDs = { id: userId, gclidValue: glidCookieValue, @@ -544,11 +539,12 @@ export class ConnectionController { masterPwd: masterPwd, }, }; - const errors = this.validateParameters(inputData.connection_parameters); - if (errors.length > 0) { + try { + await validateCreateConnectionData(inputData); + } catch (e) { return { result: false, - message: toPrettyErrorsMsg(errors), + message: e?.response?.message || e?.message || Messages.CONNECTION_TYPE_INVALID, }; } const result = await this.testConnectionUseCase.execute(inputData, InTransactionEnum.OFF); @@ -633,17 +629,15 @@ export class ConnectionController { }, }; - const errors = this.validateParameters(connectionData.connection_parameters); - - if (!connectionId) errors.push(Messages.ID_MISSING); + if (!connectionId) { + throw new BadRequestException(Messages.ID_MISSING); + } if (connectionData.connection_parameters.masterEncryption && !masterPwd) { - errors.push(Messages.MASTER_PASSWORD_REQUIRED); + throw new BadRequestException(Messages.MASTER_PASSWORD_REQUIRED); } - if (errors.length > 0) { - throw new BadRequestException(toPrettyErrorsMsg(errors)); - } + await validateCreateConnectionData(connectionData); return await this.restoreConnectionUseCase.execute(connectionData, InTransactionEnum.ON); } @@ -696,64 +690,4 @@ export class ConnectionController { return await this.unfreezeConnectionUseCase.execute({ connectionId, userId }, InTransactionEnum.ON); } - private validateParameters = (connectionData: CreateConnectionDto): Array => { - const errors = []; - - function validateConnectionType(type: string): string { - return Object.keys(ConnectionTypesEnum).find((key) => key === type); - } - - if (!connectionData.type) errors.push(Messages.TYPE_MISSING); - if (!validateConnectionType(connectionData.type)) errors.push(Messages.CONNECTION_TYPE_INVALID); - if (!isConnectionEntityAgent(connectionData)) { - if (!connectionData.host) { - errors.push(Messages.HOST_MISSING); - return errors; - } - - if (isRedisConnectionUrl(connectionData.host)) { - return errors; - } - - if (!connectionData.username && connectionData.type !== ConnectionTypesEnum.redis) - errors.push(Messages.USERNAME_MISSING); - - if ( - connectionData.type === ConnectionTypesEnum.dynamodb || - connectionData.type === ConnectionTypesEnum.elasticsearch - ) { - return errors; - } - - if (!connectionData.database && connectionData.type !== ConnectionTypesEnum.redis) - errors.push(Messages.DATABASE_MISSING); - if (process.env.NODE_ENV !== 'test' && !connectionData.ssh) { - if (!this.isMongoHost(connectionData.host)) { - if (!validator.isFQDN(connectionData.host) && !validator.isIP(connectionData.host)) - errors.push(Messages.HOST_NAME_INVALID); - } - } - - if (typeof connectionData.port !== 'number') errors.push(Messages.PORT_FORMAT_INCORRECT); - if (connectionData.port < 0 || connectionData.port > 65535 || !connectionData.port) - errors.push(Messages.PORT_MISSING); - if (typeof connectionData.ssh !== 'boolean') errors.push(Messages.SSH_FORMAT_INCORRECT); - if (connectionData.ssh) { - if (typeof connectionData.sshPort !== 'number') { - errors.push(Messages.SSH_PORT_FORMAT_INCORRECT); - } - if (!connectionData.sshPort) errors.push(Messages.SSH_PORT_MISSING); - if (!connectionData.sshUsername) errors.push(Messages.SSH_USERNAME_MISSING); - if (!connectionData.sshHost) errors.push(Messages.SSH_HOST_MISSING); - } - } else { - if (!connectionData.title) errors.push('Connection title missing'); - } - - return errors; - }; - - private isMongoHost(host: string): boolean { - return host.startsWith('mongodb+srv'); - } } diff --git a/backend/src/entities/connection/connection.entity.ts b/backend/src/entities/connection/connection.entity.ts index 8dfcb6429..fe228177e 100644 --- a/backend/src/entities/connection/connection.entity.ts +++ b/backend/src/entities/connection/connection.entity.ts @@ -190,7 +190,6 @@ export class ConnectionEntity { this.authSource = foundTestConnectionByType.authSource; this.sid = foundTestConnectionByType.sid; this.schema = foundTestConnectionByType.schema; - this.cert = foundTestConnectionByType.cert; this.azure_encryption = foundTestConnectionByType.azure_encryption; } } else { diff --git a/backend/src/entities/connection/connection.module.ts b/backend/src/entities/connection/connection.module.ts index 442cafe4b..cc1adfe95 100644 --- a/backend/src/entities/connection/connection.module.ts +++ b/backend/src/entities/connection/connection.module.ts @@ -140,7 +140,6 @@ export class ConnectionModule implements NestModule { consumer .apply(AuthMiddleware) .forRoutes( - // { path: 'connections', method: RequestMethod.GET }, { path: 'connections/author', method: RequestMethod.GET }, { path: 'connection/one/:connectionId', method: RequestMethod.GET }, { path: '/connection/users/:connectionId', method: RequestMethod.GET }, diff --git a/backend/src/entities/connection/repository/custom-connection-repository-extension.ts b/backend/src/entities/connection/repository/custom-connection-repository-extension.ts index 47e210055..d9a29d05e 100644 --- a/backend/src/entities/connection/repository/custom-connection-repository-extension.ts +++ b/backend/src/entities/connection/repository/custom-connection-repository-extension.ts @@ -50,12 +50,7 @@ export const customConnectionRepositoryExtension: IConnectionRepository = { }, async findAllUserNonTestsConnections(userId: string): Promise> { - const connectionQb = this.createQueryBuilder('connection') - .leftJoinAndSelect('connection.groups', 'group') - .leftJoinAndSelect('group.users', 'user') - .andWhere('user.id = :userId', { userId: userId }) - .andWhere('connection.isTestConnection = :isTest', { isTest: false }); - return await connectionQb.getMany(); + return await this.findAllUserConnections(userId, false); }, async findAllUsersInConnection(connectionId: string): Promise> { diff --git a/backend/src/entities/connection/ssl-certificate/read-certificate.ts b/backend/src/entities/connection/ssl-certificate/read-certificate.ts index b1f42e6c0..e37605975 100644 --- a/backend/src/entities/connection/ssl-certificate/read-certificate.ts +++ b/backend/src/entities/connection/ssl-certificate/read-certificate.ts @@ -5,7 +5,12 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +let cachedCertificate: string | null = null; + export async function readSslCertificate(): Promise { + if (cachedCertificate) { + return cachedCertificate; + } const fileName = 'global-bundle.pem'; return new Promise((resolve, reject) => { fs.readFile( @@ -13,8 +18,9 @@ export async function readSslCertificate(): Promise { 'utf8', (err, data) => { if (err) { - reject(err); + return reject(err); } + cachedCertificate = data; resolve(data); }, ); diff --git a/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts b/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts index a46b46bee..32962d25e 100644 --- a/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts @@ -3,6 +3,7 @@ import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-acce import AbstractUseCase from '../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../common/data-injection.tokens.js'; +import { AccessLevelEnum } from '../../../enums/access-level.enum.js'; import { TablePermissionDs } from '../../permission/application/data-structures/create-permissions.ds.js'; import { FoundPermissionsInConnectionDs } from '../application/data-structures/found-permissions-in-connection.ds.js'; import { GetPermissionsInConnectionDs } from '../application/data-structures/get-permissions-in-connection.ds.js'; @@ -36,15 +37,24 @@ export class GetPermissionsForGroupInConnectionUseCase const dao = getDataAccessObject(connection); const tables: Array = (await dao.getTablesFromDB()).map((table) => table.tableName); - const tablesWithAccessLevels: Array = await Promise.all( - tables.map(async (table: string) => { - return await this._dbContext.permissionRepository.getGroupPermissionsForTable( - inputData.connectionId, - inputData.groupId, - table, - ); - }), + const allTablePermissions = await this._dbContext.permissionRepository.getGroupPermissionsForAllTables( + inputData.connectionId, + inputData.groupId, ); + + const tablesWithAccessLevels: Array = tables.map((tableName) => { + const tablePermissions = allTablePermissions.filter((p) => p.tableName === tableName); + return { + tableName, + accessLevel: { + add: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.add), + delete: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.delete), + edit: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.edit), + readonly: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.readonly), + visibility: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.visibility), + }, + }; + }); const allTableSettingsInConnection = await this._dbContext.tableSettingsRepository.findTableSettingsInConnection( inputData.connectionId, ); diff --git a/backend/src/entities/connection/use-cases/test-connection.use.case.ts b/backend/src/entities/connection/use-cases/test-connection.use.case.ts index 26109543a..434150131 100644 --- a/backend/src/entities/connection/use-cases/test-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/test-connection.use.case.ts @@ -108,12 +108,13 @@ export class TestConnectionUseCase }; } - let updated: any = Object.assign(toUpdate, connectionData); + const updated = Object.assign(toUpdate, connectionData); const dataForProcessing: CreateConnectionDs = { connection_parameters: updated, creation_info: null, }; - updated = (await processAWSConnection(dataForProcessing)).connection_parameters; + const processed = (await processAWSConnection(dataForProcessing)).connection_parameters; + Object.assign(updated, processed); const dao = getDataAccessObject(updated); return await this.testConnection( @@ -125,7 +126,7 @@ export class TestConnectionUseCase } catch (e) { return { result: false, - message: `${Messages.CONNECTION_TEST_FILED}${e ? e : ''}`, + message: `${Messages.CONNECTION_TEST_FAILED}${e ? e : ''}`, }; } } else { @@ -157,8 +158,8 @@ export class TestConnectionUseCase } private async testConnection( - dao: any, - connectionData: any, + dao: { testConnect: () => Promise }, + connectionData: UpdateConnectionDs['connection_parameters'], authorId: string, connectionType: ConnectionTypesEnum, ): Promise { diff --git a/backend/src/entities/connection/use-cases/update-connection.use.case.ts b/backend/src/entities/connection/use-cases/update-connection.use.case.ts index 7f6723f46..5658dbcbe 100644 --- a/backend/src/entities/connection/use-cases/update-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/update-connection.use.case.ts @@ -8,7 +8,6 @@ import { Messages } from '../../../exceptions/text/messages.js'; import { Constants } from '../../../helpers/constants/constants.js'; import { Encryptor } from '../../../helpers/encryption/encryptor.js'; import { isConnectionTypeAgent } from '../../../helpers/index.js'; -// import { SaasCompanyGatewayService } from '../../../microservices/gateways/saas-gateway.ts/saas-company-gateway.service.js'; import { AmplitudeService } from '../../amplitude/amplitude.service.js'; import { UpdateConnectionDs } from '../application/data-structures/update-connection.ds.js'; import { CreatedConnectionDTO } from '../application/dto/created-connection.dto.js'; @@ -27,7 +26,6 @@ export class UpdateConnectionUseCase @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, private readonly amplitudeService: AmplitudeService, - // private readonly saasCompanyGatewayService: SaasCompanyGatewayService, ) { super(); } @@ -41,18 +39,6 @@ export class UpdateConnectionUseCase let { connection_parameters } = updateConnectionData; await validateCreateConnectionData(updateConnectionData); - // if (isSaaS()) { - // const userCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(authorId); - // const companyInfoFromSaas = await this.saasCompanyGatewayService.getCompanyInfo(userCompany.id); - // if (companyInfoFromSaas.subscriptionLevel === SubscriptionLevelEnum.FREE_PLAN) { - // if (Constants.NON_FREE_PLAN_CONNECTION_TYPES.includes(updateConnectionData.connection_parameters.type)) { - // throw new NonAvailableInFreePlanException( - // Messages.CANNOT_CREATE_CONNECTION_THIS_TYPE_IN_FREE_PLAN(updateConnectionData.connection_parameters.type), - // ); - // } - // } - // } - const foundConnectionToUpdate = await this._dbContext.connectionRepository.findAndDecryptConnection( connectionId, masterPwd, @@ -94,6 +80,15 @@ export class UpdateConnectionUseCase updatedConnection = this.clearNonRequiredConnectionAgentProperties(updatedConnection); } if (updatedConnection.masterEncryption && masterPwd) { + if (updatedConnection.master_hash) { + const isMasterPwdValid = await Encryptor.verifyUserPassword(masterPwd, updatedConnection.master_hash); + if (!isMasterPwdValid) { + throw new HttpException( + { message: Messages.MASTER_PASSWORD_INCORRECT }, + HttpStatus.BAD_REQUEST, + ); + } + } updatedConnection = Encryptor.encryptConnectionCredentials(updatedConnection, masterPwd); updatedConnection.master_hash = await Encryptor.hashUserPassword(masterPwd); } diff --git a/backend/src/entities/connection/utils/is-host-allowed.ts b/backend/src/entities/connection/utils/is-host-allowed.ts index e04156fc3..be3d2bca5 100644 --- a/backend/src/entities/connection/utils/is-host-allowed.ts +++ b/backend/src/entities/connection/utils/is-host-allowed.ts @@ -1,12 +1,21 @@ +import { HttpStatus } from '@nestjs/common'; +import { HttpException } from '@nestjs/common/exceptions/http.exception.js'; import dns from 'dns'; import ipRangeCheck from 'ip-range-check'; +import { Messages } from '../../../exceptions/text/messages.js'; import { isSaaS } from '../../../helpers/app/is-saas.js'; import { Constants } from '../../../helpers/constants/constants.js'; -import { isConnectionEntityAgent } from '../../../helpers/index.js'; -import { CreateConnectionDto } from '../application/dto/create-connection.dto.js'; +import { isConnectionTypeAgent } from '../../../helpers/index.js'; -export async function isHostAllowed(connectionData: CreateConnectionDto): Promise { - if (isConnectionEntityAgent(connectionData) || process.env.NODE_ENV === 'test') { +export interface HostCheckData { + type?: string; + host?: string; + ssh?: boolean; + sshHost?: string; +} + +export async function isHostAllowed(connectionData: HostCheckData): Promise { + if (isConnectionTypeAgent(connectionData.type) || process.env.NODE_ENV === 'test') { return true; } if (process.env.NODE_ENV !== 'test' && !isSaaS()) { @@ -17,29 +26,31 @@ export async function isHostAllowed(connectionData: CreateConnectionDto): Promis const testHosts = Constants.getTestConnectionsHostNamesArr(); if (!connectionData.ssh) { dns.lookup(connectionData.host, (err, address) => { + if (err) { + return reject(err); + } if (ipRangeCheck(address, Constants.FORBIDDEN_HOSTS) && !testHosts.includes(connectionData.host)) { resolve(false); } else { resolve(true); } - if (err) { - reject(err); - } }); } else if (connectionData.ssh && connectionData.sshHost) { dns.lookup(connectionData.sshHost, (err, address) => { + if (err) { + return reject(err); + } if (ipRangeCheck(address, Constants.FORBIDDEN_HOSTS) && !testHosts.includes(connectionData.host)) { resolve(false); } else { resolve(true); } - if (err) { - reject(err); - } }); } - }).catch((e) => { - console.error('DNS lookup error message', e.message); - return false; + }).catch((_e) => { + throw new HttpException( + { message: Messages.CANNOT_CREATE_CONNECTION_TO_THIS_HOST }, + HttpStatus.FORBIDDEN, + ); }); } diff --git a/backend/src/entities/connection/utils/validate-create-connection-data.ts b/backend/src/entities/connection/utils/validate-create-connection-data.ts index 1d5d65873..232fd5874 100644 --- a/backend/src/entities/connection/utils/validate-create-connection-data.ts +++ b/backend/src/entities/connection/utils/validate-create-connection-data.ts @@ -1,17 +1,16 @@ import { HttpStatus } from '@nestjs/common'; import { HttpException } from '@nestjs/common/exceptions/http.exception.js'; +import { isRedisConnectionUrl } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; import { ConnectionTypesEnum, ConnectionTypeTestEnum, } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; -import dns from 'dns'; -import ipRangeCheck from 'ip-range-check'; +import validator from 'validator'; import { Messages } from '../../../exceptions/text/messages.js'; -import { isSaaS } from '../../../helpers/app/is-saas.js'; -import { Constants } from '../../../helpers/constants/constants.js'; import { isConnectionTypeAgent, toPrettyErrorsMsg } from '../../../helpers/index.js'; import { CreateConnectionDs } from '../application/data-structures/create-connection.ds.js'; import { UpdateConnectionDs } from '../application/data-structures/update-connection.ds.js'; +import { isHostAllowed } from './is-host-allowed.js'; export async function validateCreateConnectionData( createConnectionData: CreateConnectionDs | UpdateConnectionDs, @@ -26,17 +25,17 @@ export async function validateCreateConnectionData( if (!host) { errors.push(Messages.HOST_MISSING); } - // if (ssh) { - // if (!validator.isFQDN(host) && !validator.isIP(host) && !host.includes('localhost') && !host.includes('127.0.0.1')) { - // errors.push(Messages.HOST_NAME_INVALID); - // throw new HttpException( - // { - // message: toPrettyErrorsMsg(errors), - // }, - // HttpStatus.BAD_REQUEST, - // ); - // } - // } + + if (host && isRedisConnectionUrl(host)) { + if (errors.length > 0) { + throw new HttpException( + { message: toPrettyErrorsMsg(errors) }, + HttpStatus.BAD_REQUEST, + ); + } + return true; + } + if (!username && type !== ConnectionTypesEnum.redis) errors.push(Messages.USERNAME_MISSING); if ( @@ -45,6 +44,15 @@ export async function validateCreateConnectionData( type !== ConnectionTypesEnum.redis ) { if (!database) errors.push(Messages.DATABASE_MISSING); + + if (process.env.NODE_ENV !== 'test' && !ssh && host) { + if (!host.startsWith('mongodb+srv')) { + if (!validator.isFQDN(host) && !validator.isIP(host)) { + errors.push(Messages.HOST_NAME_INVALID); + } + } + } + if (port < 0 || port > 65535 || !port) errors.push(Messages.PORT_MISSING); if (typeof port !== 'number') errors.push(Messages.PORT_FORMAT_INCORRECT); if (typeof ssh !== 'boolean') errors.push(Messages.SSH_FORMAT_INCORRECT); @@ -69,7 +77,7 @@ export async function validateCreateConnectionData( HttpStatus.BAD_REQUEST, ); } else { - const checkingResult = await checkIsHostAllowed(createConnectionData); + const checkingResult = await isHostAllowed(createConnectionData.connection_parameters); if (!checkingResult) { throw new HttpException( { @@ -88,47 +96,3 @@ function validateConnectionType(type: string): string { } return Object.keys(ConnectionTypesEnum).find((key) => key === type); } - -async function checkIsHostAllowed(createConnectionData: CreateConnectionDs | UpdateConnectionDs): Promise { - if (isConnectionTypeAgent(createConnectionData.connection_parameters.type) || process.env.NODE_ENV === 'test') { - return true; - } - if (process.env.NODE_ENV !== 'test' && !isSaaS()) { - return true; - } - return new Promise((resolve, reject) => { - const testHosts = Constants.getTestConnectionsHostNamesArr(); - if (!createConnectionData.connection_parameters.ssh) { - dns.lookup(createConnectionData.connection_parameters.host, (err, address) => { - if ( - ipRangeCheck(address, Constants.FORBIDDEN_HOSTS) && - !testHosts.includes(createConnectionData.connection_parameters.host) - ) { - resolve(false); - } else { - resolve(true); - } - if (err) { - reject(err); - } - }); - } else if (createConnectionData.connection_parameters.ssh && createConnectionData.connection_parameters.sshHost) { - dns.lookup(createConnectionData.connection_parameters.sshHost, (err, address) => { - if ( - ipRangeCheck(address, Constants.FORBIDDEN_HOSTS) && - !testHosts.includes(createConnectionData.connection_parameters.host) - ) { - resolve(false); - } else { - resolve(true); - } - if (err) { - reject(err); - } - }); - } - }).catch((e) => { - console.error('DNS lookup error message', e.message); - return false; - }); -} diff --git a/backend/src/exceptions/text/messages.ts b/backend/src/exceptions/text/messages.ts index 0d21393d8..23abdd172 100644 --- a/backend/src/exceptions/text/messages.ts +++ b/backend/src/exceptions/text/messages.ts @@ -85,7 +85,7 @@ export const Messages = { CONNECTION_NOT_ENCRYPTED: 'Connection is not encrypted', CONNECTION_MASTER_PASSWORD_NOT_SET: 'Connection master password is not set (or connection created before this feature)', - CONNECTION_TEST_FILED: 'Connection test failed. ', + CONNECTION_TEST_FAILED: 'Connection test failed. ', CONNECTION_TYPE_INVALID: `Unsupported database type. Now we supports ${enumToString(ConnectionTypesEnum)}`, CONNECTION_PROPERTIES_INVALID: 'Connection properties are invalid', CONNECTION_PROPERTIES_CANT_BE_EMPTY: `Connection properties cannot be empty`,