Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions redisinsight/api/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default {
requestTimeout: parseInt(process.env.RI_REQUEST_TIMEOUT, 10) || 25000,
excludeRoutes: [],
excludeAuthRoutes: [],
databaseManagement: process.env.RI_DATABASE_MANAGEMENT !== 'false',
},
statics: {
initDefaults: process.env.RI_STATICS_INIT_DEFAULTS ? process.env.RI_STATICS_INIT_DEFAULTS === 'true' : true,
Expand Down
4 changes: 4 additions & 0 deletions redisinsight/api/src/__mocks__/database-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,7 @@ export const mockSshImportService = jest.fn(() => ({
id: undefined,
}),
}));

export const mockDatabaseImportService = jest.fn(() => ({
import: jest.fn(),
}));
12 changes: 12 additions & 0 deletions redisinsight/api/src/__mocks__/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { CloudDatabaseDetailsEntity } from 'src/modules/cloud/database/entities/
import { mockCloudDatabaseDetails, mockCloudDatabaseDetailsEntity } from 'src/__mocks__/cloud-database';
import { mockRedisClientListResult } from 'src/__mocks__/database-info';
import { DatabaseOverviewKeyspace } from 'src/modules/database/constants/overview';
import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto';

export const mockDatabaseId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-db-id';

Expand All @@ -43,6 +44,12 @@ export const mockDatabase = Object.assign(new Database(), {
version: '7.0',
});

export const mockCreateDatabaseDto = Object.assign(new CreateDatabaseDto(), {
name: mockDatabase.name,
host: mockDatabase.host,
port: mockDatabase.port,
});

export const mockDatabaseModules = [
{
name: 'rg',
Expand Down Expand Up @@ -267,6 +274,11 @@ export const mockDatabaseRepository = jest.fn(() => ({
export const mockDatabaseService = jest.fn(() => ({
get: jest.fn().mockResolvedValue(mockDatabase),
create: jest.fn().mockResolvedValue(mockDatabase),
update: jest.fn().mockResolvedValue(mockDatabase),
clone: jest.fn().mockResolvedValue(mockDatabase),
testConnection: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
bulkDelete: jest.fn().mockResolvedValue({ affected: 0 }),
list: jest.fn(),
}));

Expand Down
5 changes: 5 additions & 0 deletions redisinsight/api/src/__mocks__/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ export const mockFeatureSso = Object.assign(new Feature(), {
},
});

export const mockFeatureDatabaseManagement = Object.assign(new Feature(), {
name: KnownFeatures.DatabaseManagement,
flag: true,
});

export const mockFeatureRedisClient = Object.assign(new Feature(), {
name: KnownFeatures.RedisClient,
flag: true,
Expand Down
13 changes: 13 additions & 0 deletions redisinsight/api/src/__mocks__/redis-enterprise.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RedisEnterpriseDatabase } from 'src/modules/redis-enterprise/dto/cluster.dto';
import { RedisEnterpriseDatabaseStatus } from 'src/modules/redis-enterprise/models/redis-enterprise-database';
import { AddRedisEnterpriseDatabasesDto } from 'src/modules/redis-enterprise/dto/redis-enterprise-cluster.dto';

export const mockRedisEnterpriseDatabaseDto: RedisEnterpriseDatabase = {
uid: 1,
Expand All @@ -14,7 +15,19 @@ export const mockRedisEnterpriseDatabaseDto: RedisEnterpriseDatabase = {
password: null,
};

export const mockAddRedisEnterpriseDatabasesDto = Object.assign(new AddRedisEnterpriseDatabasesDto(), {
host: 'localhost',
port: 9443,
username: 'admin',
password: 'password',
uids: [1],
});

export const mockRedisEnterpriseAnalytics = jest.fn(() => ({
sendGetREClusterDbsSucceedEvent: jest.fn(),
sendGetREClusterDbsFailedEvent: jest.fn(),
}));

export const mockRedisEnterpriseService = jest.fn(() => ({
addRedisEnterpriseDatabases: jest.fn().mockResolvedValue([]),
}));
16 changes: 16 additions & 0 deletions redisinsight/api/src/__mocks__/redis-sentinel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SentinelMaster, SentinelMasterStatus } from 'src/modules/redis-sentinel/models/sentinel-master';
import { Endpoint } from 'src/common/models';
import { CreateSentinelDatabasesDto } from 'src/modules/redis-sentinel/dto/create.sentinel.databases.dto';

export const mockOtherSentinelsReply = [[
'ip',
Expand Down Expand Up @@ -27,7 +28,22 @@ export const mockSentinelMasterDto: SentinelMaster = {
nodes: [mockOtherSentinelEndpoint],
};

export const mockCreateSentinelDatabasesDto = Object.assign(new CreateSentinelDatabasesDto(), {
...mockOtherSentinelEndpoint,
masters: [
{
name: mockSentinelMasterDto.name,
alias: mockSentinelMasterDto.name,
},
],
});

export const mockRedisSentinelAnalytics = jest.fn(() => ({
sendGetSentinelMastersSucceedEvent: jest.fn(),
sendGetSentinelMastersFailedEvent: jest.fn(),
}));

export const mockRedisSentinelService = jest.fn(() => ({
getSentinelMasters: jest.fn().mockResolvedValue([mockSentinelMasterDto]),
createSentinelDatabases: jest.fn().mockResolvedValue([]),
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { applyDecorators, UseGuards } from '@nestjs/common';
import { DatabaseManagementGuard } from 'src/common/guards/database-management.guard';

export function DatabaseManagement() {
return applyDecorators(
UseGuards(new DatabaseManagementGuard()),
);
}
1 change: 1 addition & 0 deletions redisinsight/api/src/common/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './object-as-map.decorator';
export * from './is-multi-number.decorator';
export * from './is-bigger-than.decorator';
export * from './is-github-link.decorator';
export * from './database-management.decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { when } from 'jest-when';
import { DatabaseManagementGuard } from 'src/common/guards/database-management.guard';
import { ForbiddenException } from '@nestjs/common';
import config, { Config } from 'src/utils/config';

const mockServerConfig = config.get('server') as Config['server'];

jest.mock('src/utils/config', jest.fn(
() => jest.requireActual('src/utils/config') as object,
));

describe('DatabaseManagementGuard', () => {
let guard: DatabaseManagementGuard;
let configGetSpy: jest.SpyInstance;

beforeEach(() => {
jest.clearAllMocks();
configGetSpy = jest.spyOn(config, 'get');

when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);

guard = new DatabaseManagementGuard();
});

it('should return true', () => {
mockServerConfig.databaseManagement = true;

expect(guard.canActivate()).toEqual(true);
});

it('should throw an error when database management is disabled', () => {
mockServerConfig.databaseManagement = false;

expect(guard.canActivate).toThrowError(new ForbiddenException('Database connection management is disabled.'));
});
});
16 changes: 16 additions & 0 deletions redisinsight/api/src/common/guards/database-management.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CanActivate, ForbiddenException, Injectable } from '@nestjs/common';
import config, { Config } from 'src/utils/config';
import ERROR_MESSAGES from 'src/constants/error-messages';

const SERVER_CONFIG = config.get('server') as Config['server'];

@Injectable()
export class DatabaseManagementGuard implements CanActivate {
canActivate(): boolean {
if (!SERVER_CONFIG.databaseManagement) {
throw new ForbiddenException(ERROR_MESSAGES.DATABASE_MANAGEMENT_IS_DISABLED);
}

return true;
}
}
1 change: 1 addition & 0 deletions redisinsight/api/src/constants/custom-error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export enum CustomErrorCodes {
CloudTaskNotFound = 11_112,
CloudJobNotFound = 11_113,
CloudSubscriptionAlreadyExistsFree = 11_114,
CloudDatabaseImportForbidden = 11_115,

// General database errors [11200, 11299]
DatabaseAlreadyExists = 11_200,
Expand Down
2 changes: 2 additions & 0 deletions redisinsight/api/src/constants/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default {
INCORRECT_CERTIFICATES: (url) => `Could not connect to ${url}, please check the CA or Client certificate.`,
INCORRECT_CREDENTIALS: (url) => `Could not connect to ${url}, please check the Username or Password.`,

DATABASE_MANAGEMENT_IS_DISABLED: 'Database connection management is disabled.',
CA_CERT_EXIST: 'This ca certificate name is already in use.',
INVALID_CA_BODY: 'Invalid CA body',
INVALID_SSH_PRIVATE_KEY_BODY: 'Invalid SSH private key body',
Expand Down Expand Up @@ -111,6 +112,7 @@ export default {
CLOUD_DATABASE_IN_FAILED_STATE: 'Cloud database is in the failed state',
CLOUD_DATABASE_IN_UNEXPECTED_STATE: 'Cloud database is in unexpected state',
CLOUD_DATABASE_ALREADY_EXISTS_FREE: 'Free trial database already exists',
CLOUD_DATABASE_IMPORT_FORBIDDEN: 'Adding your Redis Cloud database to Redis Insight is disabled due to a setting restricting database connection management.',
CLOUD_PLAN_NOT_FOUND_FREE: 'Unable to find free cloud plan',
CLOUD_SUBSCRIPTION_ALREADY_EXISTS_FREE: 'Free subscription already exists',
COMMON_DEFAULT_IMPORT_ERROR: 'Unable to import default data',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,4 +313,4 @@ export class KeysService {
throw catchAclError(error);
}
}
}
}
5 changes: 5 additions & 0 deletions redisinsight/api/src/modules/cloud/job/cloud-job.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.s
import { CloudSubscriptionApiService } from 'src/modules/cloud/subscription/cloud-subscription.api.service';
import { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';
import { DatabaseInfoService } from 'src/modules/database/database-info.service';
import { FeatureService } from 'src/modules/feature/feature.service';

@Injectable()
export class CloudJobFactory {
Expand All @@ -32,6 +33,7 @@ export class CloudJobFactory {
private readonly bulkImportService: BulkImportService,
private readonly cloudCapiKeyService: CloudCapiKeyService,
private readonly cloudSubscriptionApiService: CloudSubscriptionApiService,
private readonly featureService: FeatureService,
) {}

async create(
Expand Down Expand Up @@ -62,6 +64,7 @@ export class CloudJobFactory {
bulkImportService: this.bulkImportService,
cloudCapiKeyService: this.cloudCapiKeyService,
cloudSubscriptionApiService: this.cloudSubscriptionApiService,
featureService: this.featureService,
},
);
case CloudJobName.CreateFreeDatabase:
Expand All @@ -80,6 +83,7 @@ export class CloudJobFactory {
databaseInfoService: this.databaseInfoService,
bulkImportService: this.bulkImportService,
cloudCapiKeyService: this.cloudCapiKeyService,
featureService: this.featureService,
},
);
case CloudJobName.ImportFreeDatabase:
Expand All @@ -96,6 +100,7 @@ export class CloudJobFactory {
cloudDatabaseAnalytics: this.cloudDatabaseAnalytics,
databaseService: this.databaseService,
cloudCapiKeyService: this.cloudCapiKeyService,
featureService: this.featureService,
},
);
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common';
import ERROR_MESSAGES from 'src/constants/error-messages';
import { CustomErrorCodes } from 'src/constants';

export class CloudDatabaseImportForbiddenException extends HttpException {
constructor(
message = ERROR_MESSAGES.CLOUD_DATABASE_IMPORT_FORBIDDEN,
options?: HttpExceptionOptions,
) {
const response = {
message,
statusCode: HttpStatus.FORBIDDEN,
error: 'CloudDatabaseImportForbidden',
errorCode: CustomErrorCodes.CloudDatabaseImportForbidden,
};

super(response, response.statusCode, options);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './cloud-database-already-exists-free.exception';
export * from './cloud-database-import-forbidden.exception';
export * from './cloud-database-in-failed-state.exception';
export * from './cloud-database-in-unexpected-state.exception';
export * from './cloud-job.error.handler';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { WaitForActiveDatabaseCloudJob } from 'src/modules/cloud/job/jobs/wait-f
import { CloudJobName } from 'src/modules/cloud/job/constants';
import { CloudJobStatus, CloudJobStep } from 'src/modules/cloud/job/models';
import {
CloudDatabaseImportForbiddenException,
CloudJobUnexpectedErrorException,
CloudTaskNoResourceIdException,
} from 'src/modules/cloud/job/exceptions';
Expand All @@ -22,6 +23,8 @@ import { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.s
import { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';
import { ClientContext, SessionMetadata } from 'src/common/models';
import { DatabaseInfoService } from 'src/modules/database/database-info.service';
import { FeatureService } from 'src/modules/feature/feature.service';
import { KnownFeatures } from 'src/modules/feature/constants';

const cloudConfig = config.get('cloud');

Expand All @@ -42,6 +45,7 @@ export class CreateFreeDatabaseCloudJob extends CloudJob {
databaseInfoService: DatabaseInfoService,
bulkImportService: BulkImportService,
cloudCapiKeyService: CloudCapiKeyService,
featureService: FeatureService,
},
) {
super(options);
Expand Down Expand Up @@ -113,6 +117,15 @@ export class CreateFreeDatabaseCloudJob extends CloudJob {

this.checkSignal();

const isDatabaseManagementEnabled = await this.dependencies.featureService.isFeatureEnabled(
sessionMetadata,
KnownFeatures.DatabaseManagement,
);

if (!isDatabaseManagementEnabled) {
throw new CloudDatabaseImportForbiddenException();
}

const {
publicEndpoint,
name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CloudSubscription } from 'src/modules/cloud/subscription/models';
import { DatabaseInfoService } from 'src/modules/database/database-info.service';
import { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';
import { SessionMetadata } from 'src/common/models';
import { FeatureService } from 'src/modules/feature/feature.service';
import { CloudSubscriptionApiService } from '../../subscription/cloud-subscription.api.service';
import { CloudSubscriptionPlanResponse } from '../../subscription/dto';

Expand All @@ -38,6 +39,7 @@ export class CreateFreeSubscriptionAndDatabaseCloudJob extends CloudJob {
bulkImportService: BulkImportService,
cloudCapiKeyService: CloudCapiKeyService,
cloudSubscriptionApiService: CloudSubscriptionApiService,
featureService: FeatureService,
},
) {
super(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import config from 'src/utils/config';
import { CloudDatabaseAnalytics } from 'src/modules/cloud/database/cloud-database.analytics';
import { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';
import { SessionMetadata } from 'src/common/models';
import { KnownFeatures } from 'src/modules/feature/constants';
import { FeatureService } from 'src/modules/feature/feature.service';
import { CloudDatabaseImportForbiddenException } from 'src/modules/cloud/job/exceptions';

const cloudConfig = config.get('cloud');

Expand All @@ -35,6 +38,7 @@ export class ImportFreeDatabaseCloudJob extends CloudJob {
cloudDatabaseAnalytics: CloudDatabaseAnalytics,
databaseService: DatabaseService,
cloudCapiKeyService: CloudCapiKeyService,
featureService: FeatureService,
},
) {
super(options);
Expand Down Expand Up @@ -66,6 +70,15 @@ export class ImportFreeDatabaseCloudJob extends CloudJob {

this.checkSignal();

const isDatabaseManagementEnabled = await this.dependencies.featureService.isFeatureEnabled(
sessionMetadata,
KnownFeatures.DatabaseManagement,
);

if (!isDatabaseManagementEnabled) {
throw new CloudDatabaseImportForbiddenException();
}

const {
publicEndpoint,
name,
Expand Down
Loading
Loading