diff --git a/redisinsight/api/src/modules/database/database.controller.ts b/redisinsight/api/src/modules/database/database.controller.ts index b4f86075eb..532b512520 100644 --- a/redisinsight/api/src/modules/database/database.controller.ts +++ b/redisinsight/api/src/modules/database/database.controller.ts @@ -1,7 +1,17 @@ import { ApiTags } from '@nestjs/swagger'; import { Body, - ClassSerializerInterceptor, Controller, Delete, Get, Param, Post, Put, UseInterceptors, UsePipes, ValidationPipe, + ClassSerializerInterceptor, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Put, + UseInterceptors, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { Database } from 'src/modules/database/models/database'; @@ -16,6 +26,7 @@ import { DeleteDatabasesDto } from 'src/modules/database/dto/delete.databases.dt import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; import { ClientMetadataParam } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; +import { ModifyDatabaseDto } from 'src/modules/database/dto/modify.database.dto'; @ApiTags('Database') @Controller('databases') @@ -117,6 +128,34 @@ export class DatabaseController { return await this.service.update(id, database, true); } + @UseInterceptors(ClassSerializerInterceptor) + @UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT)) + @Patch(':id') + @ApiEndpoint({ + description: 'Update database instance by id', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Updated database instance\' response', + type: Database, + }, + ], + }) + @UsePipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }), + ) + async modify( + @Param('id') id: string, + @Body() database: ModifyDatabaseDto, + ): Promise { + return await this.service.update(id, database, true); + } + @Delete('/:id') @ApiEndpoint({ statusCode: 200, diff --git a/redisinsight/api/src/modules/database/database.service.ts b/redisinsight/api/src/modules/database/database.service.ts index 8c9800fc85..6f5aaacb7b 100644 --- a/redisinsight/api/src/modules/database/database.service.ts +++ b/redisinsight/api/src/modules/database/database.service.ts @@ -18,6 +18,7 @@ import { AppRedisInstanceEvents } from 'src/constants'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; import { ClientContext } from 'src/common/models'; +import { ModifyDatabaseDto } from 'src/modules/database/dto/modify.database.dto'; @Injectable() export class DatabaseService { @@ -118,7 +119,7 @@ export class DatabaseService { // todo: remove manualUpdate flag logic public async update( id: string, - dto: UpdateDatabaseDto, + dto: UpdateDatabaseDto | ModifyDatabaseDto, manualUpdate: boolean = true, ): Promise { this.logger.log(`Updating database: ${id}`); diff --git a/redisinsight/api/src/modules/database/dto/modify.database.dto.ts b/redisinsight/api/src/modules/database/dto/modify.database.dto.ts new file mode 100644 index 0000000000..a85e018b43 --- /dev/null +++ b/redisinsight/api/src/modules/database/dto/modify.database.dto.ts @@ -0,0 +1,20 @@ +import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto'; +import { PartialType } from '@nestjs/swagger'; +import { + IsInt, IsString, MaxLength, ValidateIf, +} from 'class-validator'; + +export class ModifyDatabaseDto extends PartialType(CreateDatabaseDto) { + @ValidateIf((object, value) => value !== undefined) + @IsString({ always: true }) + @MaxLength(500) + name: string; + + @ValidateIf((object, value) => value !== undefined) + @IsString({ always: true }) + host: string; + + @ValidateIf((object, value) => value !== undefined) + @IsInt({ always: true }) + port: number; +} diff --git a/redisinsight/api/src/modules/database/dto/update.database.dto.ts b/redisinsight/api/src/modules/database/dto/update.database.dto.ts index 1f50ed47f3..9946f1a5cf 100644 --- a/redisinsight/api/src/modules/database/dto/update.database.dto.ts +++ b/redisinsight/api/src/modules/database/dto/update.database.dto.ts @@ -1,10 +1,20 @@ -import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto'; -import { PartialType } from '@nestjs/swagger'; +import { ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { - IsInt, IsString, MaxLength, ValidateIf, + IsBoolean, + IsInt, IsNotEmpty, IsNotEmptyObject, IsOptional, IsString, MaxLength, Min, ValidateIf, ValidateNested, } from 'class-validator'; +import { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto'; +import { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto'; +import { Expose, Type } from 'class-transformer'; +import { caCertTransformer } from 'src/modules/certificate/transformers/ca-cert.transformer'; +import { Default } from 'src/common/decorators'; +import { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto'; +import { clientCertTransformer } from 'src/modules/certificate/transformers/client-cert.transformer'; +import { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto'; +import { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master'; +import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto'; -export class UpdateDatabaseDto extends PartialType(CreateDatabaseDto) { +export class UpdateDatabaseDto extends CreateDatabaseDto { @ValidateIf((object, value) => value !== undefined) @IsString({ always: true }) @MaxLength(500) @@ -17,4 +27,82 @@ export class UpdateDatabaseDto extends PartialType(CreateDatabaseDto) { @ValidateIf((object, value) => value !== undefined) @IsInt({ always: true }) port: number; + + @ApiPropertyOptional({ + description: 'Logical database number.', + type: Number, + }) + @IsInt() + @Min(0) + @IsOptional() + @Default(null) + db?: number; + + @ApiPropertyOptional({ + description: 'Use TLS to connect.', + type: Boolean, + }) + @IsBoolean() + @IsOptional() + @Default(false) + tls?: boolean; + + @ApiPropertyOptional({ + description: 'SNI servername', + type: String, + }) + @IsString() + @IsNotEmpty() + @IsOptional() + @Default(null) + tlsServername?: string; + + @ApiPropertyOptional({ + description: 'The certificate returned by the server needs to be verified.', + type: Boolean, + default: false, + }) + @IsOptional() + @IsBoolean({ always: true }) + @Default(false) + verifyServerCert?: boolean; + + @ApiPropertyOptional({ + description: 'CA Certificate', + oneOf: [ + { $ref: getSchemaPath(CreateCaCertificateDto) }, + { $ref: getSchemaPath(UseCaCertificateDto) }, + ], + }) + @IsOptional() + @IsNotEmptyObject() + @Type(caCertTransformer) + @ValidateNested() + @Default(null) + caCert?: CreateCaCertificateDto | UseCaCertificateDto; + + @ApiPropertyOptional({ + description: 'Client Certificate', + oneOf: [ + { $ref: getSchemaPath(CreateClientCertificateDto) }, + { $ref: getSchemaPath(UseCaCertificateDto) }, + ], + }) + @IsOptional() + @IsNotEmptyObject() + @Type(clientCertTransformer) + @ValidateNested() + @Default(null) + clientCert?: CreateClientCertificateDto | UseClientCertificateDto; + + @ApiPropertyOptional({ + description: 'Redis OSS Sentinel master group.', + type: SentinelMaster, + }) + @IsOptional() + @IsNotEmptyObject() + @Type(() => SentinelMaster) + @ValidateNested() + @Default(null) + sentinelMaster?: SentinelMaster; } diff --git a/redisinsight/api/test/api/database/PATCH-databases-id.test.ts b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts new file mode 100644 index 0000000000..2414aada9e --- /dev/null +++ b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts @@ -0,0 +1,774 @@ +import { + expect, + describe, + deps, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + requirements, + getMainCheckFn, + _, it, validateApiCall, after +} from '../deps'; +import { Joi } from '../../helpers/test'; +import { databaseSchema } from './constants'; + +const { request, server, localDb, constants, rte } = deps; + +const endpoint = (id = constants.TEST_INSTANCE_ID) => request(server).patch(`/${constants.API.DATABASES}/${id}`); + +// input data schema +const dataSchema = Joi.object({ + name: Joi.string().max(500), + host: Joi.string(), + port: Joi.number().integer(), + db: Joi.number().integer().allow(null), + username: Joi.string().allow(null), + password: Joi.string().allow(null), + tls: Joi.boolean().allow(null), + tlsServername: Joi.string().allow(null), + verifyServerCert: Joi.boolean().allow(null), +}).messages({ + 'any.required': '{#label} should not be empty', +}).strict(true); + +const validInputData = { + name: constants.getRandomString(), + host: constants.getRandomString(), + port: 111, +}; + +const baseDatabaseData = { + name: 'someName', + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + username: constants.TEST_REDIS_USER || undefined, + password: constants.TEST_REDIS_PASSWORD || undefined, +} + +const responseSchema = databaseSchema.required().strict(true); + +const mainCheckFn = getMainCheckFn(endpoint); + +let oldDatabase; +let newDatabase; +describe(`PUT /databases/:id`, () => { + beforeEach(async () => await localDb.createDatabaseInstances()); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + + [ + { + name: 'should deprecate to pass both cert id and other cert fields', + data: { + ...validInputData, + caCert: { + id: 'id', + name: 'ca', + certificate: 'ca_certificate', + }, + clientCert: { + id: 'id', + name: 'client', + certificate: 'client_cert', + key: 'client_key', + }, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + checkFn: ({ body }) => { + expect(body.message).to.contain('caCert.property name should not exist'); + expect(body.message).to.contain('caCert.property certificate should not exist'); + expect(body.message).to.contain('clientCert.property name should not exist'); + expect(body.message).to.contain('clientCert.property certificate should not exist'); + expect(body.message).to.contain('clientCert.property key should not exist'); + }, + }, + ].map(mainCheckFn); + }); + describe('Common', () => { + const newName = constants.getRandomString(); + + [ + { + name: 'Should change name (only) for existing database', + data: { + name: newName, + }, + responseSchema, + before: async () => { + oldDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); + expect(oldDatabase.name).to.not.eq(newName); + }, + responseBody: { + name: newName, + }, + after: async () => { + newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); + expect(newDatabase.name).to.eq(newName); + }, + }, + { + name: 'Should return 503 error if incorrect connection data provided', + data: { + name: 'new name', + port: 1111, + }, + statusCode: 503, + responseBody: { + statusCode: 503, + // message: `Could not connect to ${constants.TEST_REDIS_HOST}:1111, please check the connection details.`, + // todo: verify error handling because right now messages are different + error: 'Service Unavailable' + }, + after: async () => { + // check that instance wasn't changed + const newDb = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); + expect(newDb.name).to.not.eql('new name'); + expect(newDb.port).to.eql(constants.TEST_REDIS_PORT); + }, + }, + { + name: 'Should return Not Found Error', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + name: 'new name', + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + message: 'Invalid database instance id.', + error: 'Not Found' + }, + }, + ].map(mainCheckFn); + }); + describe('STANDALONE', () => { + requirements('rte.type=STANDALONE'); + describe('NO AUTH', function () { + requirements('!rte.tls', '!rte.pass'); + + [ + { + name: 'Should change host and port and recalculate data such as (provider, modules, etc...)', + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + }, + responseSchema, + before: async () => { + oldDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID_3); + expect(oldDatabase.name).to.eq(constants.TEST_INSTANCE_NAME_3); + expect(oldDatabase.modules).to.eq('[]'); + expect(oldDatabase.host).to.not.eq(constants.TEST_REDIS_HOST) + expect(oldDatabase.port).to.not.eq(constants.TEST_REDIS_PORT) + }, + responseBody: { + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + username: null, + password: null, + connectionType: constants.STANDALONE, + tls: false, + verifyServerCert: false, + tlsServername: null, + }, + after: async () => { + newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID_3); + expect(newDatabase).to.contain({ + ..._.omit(oldDatabase, ['modules', 'provider', 'lastConnection', 'new']), + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + }); + }, + }, + ].map(mainCheckFn); + + describe('Enterprise', () => { + requirements('rte.re'); + it('Should throw an error if db index specified', async () => { + await validateApiCall({ + endpoint, + statusCode: 400, + data: { + db: constants.TEST_REDIS_DB_INDEX + }, + }); + }); + }); + describe('Oss', () => { + requirements('!rte.re'); + it('Update standalone with particular db index', async () => { + let addedId; + const dbName = constants.getRandomString(); + const cliUuid = constants.getRandomString(); + const browserKeyName = constants.getRandomString(); + const cliKeyName = constants.getRandomString(); + + await validateApiCall({ + endpoint, + data: { + db: constants.TEST_REDIS_DB_INDEX, + }, + responseSchema, + responseBody: { + db: constants.TEST_REDIS_DB_INDEX, + }, + checkFn: ({ body }) => { + addedId = body.id; + } + }); + + // Create string using Browser API to particular db index + await validateApiCall({ + endpoint: () => request(server).post(`/${constants.API.DATABASES}/${addedId}/string`), + statusCode: 201, + data: { + keyName: browserKeyName, + value: 'somevalue' + }, + }); + + // Create client for CLI + await validateApiCall({ + endpoint: () => request(server).patch(`/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}`), + statusCode: 200, + }); + + // Create string using CLI API to 0 db index + await validateApiCall({ + endpoint: () => request(server).post(`/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}/send-command`), + statusCode: 200, + data: { + command: `set ${cliKeyName} somevalue`, + }, + }); + + + // check data created by db index + await rte.data.executeCommand('select', `${constants.TEST_REDIS_DB_INDEX}`); + expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(1) + expect(await rte.data.executeCommand('exists', browserKeyName)).to.eql(1) + + // check data created by db index + await rte.data.executeCommand('select', '0'); + expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(0) + expect(await rte.data.executeCommand('exists', browserKeyName)).to.eql(0) + + // switch back to db index 0 + await validateApiCall({ + endpoint, + data: { + db: 0, + }, + responseSchema, + responseBody: { + db: 0, + }, + checkFn: ({ body }) => { + addedId = body.id; + } + }); + }); + }); + }); + describe('PASS', function () { + requirements('!rte.tls', 'rte.pass'); + it('Update standalone with password', async () => { + const dbName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + password: constants.TEST_REDIS_PASSWORD, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + username: null, + password: constants.TEST_REDIS_PASSWORD, + connectionType: constants.STANDALONE, + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + // todo: cover connection error for incorrect username/password + }); + describe('TLS CA', function () { + requirements('rte.tls', '!rte.tlsAuth'); + it('update standalone instance using tls without CA verify', async () => { + const dbName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: false, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: false, + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + it('Update standalone instance using tls and verify and create CA certificate (new)', async () => { + const dbName = constants.getRandomString(); + const newCaName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: newCaName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: true, + tlsServername: null, + }, + checkFn: async ({ body }) => { + expect(body.caCert.id).to.be.a('string'); + expect(body.caCert.name).to.eq(newCaName); + expect(body.caCert.certificate).to.be.undefined; + + const ca: any = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)) + .findOneBy({ id: body.caCert.id }); + + expect(ca.certificate).to.eql(localDb.encryptData(constants.TEST_REDIS_TLS_CA)); + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + it('Should throw an error without CA cert when cert validation enabled', async () => { + const dbName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + statusCode: 400, + data: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + tls: true, + verifyServerCert: true, + caCert: null, + }, + responseBody: { + statusCode: 400, + // todo: verify error handling because right now messages are different + // message: 'Could not connect to', + error: 'Bad Request' + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + }); + it('Should throw an error with invalid CA cert', async () => { + const dbName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + statusCode: 400, + data: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + tls: true, + verifyServerCert: true, + caCert: { + name: dbName, + certificate: 'invalid' + }, + }, + responseBody: { + statusCode: 400, + // todo: verify error handling because right now messages are different + // message: 'Could not connect to', + error: 'Bad Request' + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + }); + }); + describe('TLS AUTH', function () { + requirements('rte.tls', 'rte.tlsAuth'); + + let existingCACertId, existingClientCertId, existingCACertName, existingClientCertName; + + beforeEach(localDb.initAgreements); + after(localDb.initAgreements); + + // should be first test to not break other tests + it('Update standalone instance and verify users certs (new certificates !do not encrypt)', async () => { + await localDb.setAgreements({ + encryption: false, + }); + + const dbName = constants.getRandomString(); + const newCaName = constants.getRandomString(); + const newClientCertName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint, + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: newCaName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + clientCert: { + name: newClientCertName, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + }, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: true, + tlsServername: null, + }, + checkFn: async ({ body }) => { + expect(body.caCert.id).to.be.a('string'); + expect(body.caCert.name).to.eq(newCaName); + expect(body.caCert.certificate).to.be.undefined; + + expect(body.clientCert.id).to.be.a('string'); + expect(body.clientCert.name).to.deep.eq(newClientCertName); + expect(body.clientCert.certificate).to.be.undefined; + expect(body.clientCert.key).to.be.undefined; + + const ca: any = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)) + .findOneBy({ id: body.caCert.id }); + + expect(ca.certificate).to.eql(constants.TEST_REDIS_TLS_CA); + + const clientPair: any = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)) + .findOneBy({ id: body.clientCert.id }); + + expect(clientPair.certificate).to.eql(constants.TEST_USER_TLS_CERT); + expect(clientPair.key).to.eql(constants.TEST_USER_TLS_KEY); + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + it('Update standalone instance and verify users certs (new certificates)', async () => { + const dbName = constants.getRandomString(); + const newCaName = existingCACertName = constants.getRandomString(); + const newClientCertName = existingClientCertName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + const { body } = await validateApiCall({ + endpoint, + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: newCaName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + clientCert: { + name: newClientCertName, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + }, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: true, + tlsServername: null, + }, + checkFn: async ({ body }) => { + expect(body.caCert.id).to.be.a('string'); + expect(body.caCert.name).to.eq(newCaName); + expect(body.caCert.certificate).to.be.undefined; + + expect(body.clientCert.id).to.be.a('string'); + expect(body.clientCert.name).to.deep.eq(newClientCertName); + expect(body.clientCert.certificate).to.be.undefined; + expect(body.clientCert.key).to.be.undefined; + + const ca: any = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)) + .findOneBy({ id: body.caCert.id }); + + expect(ca.certificate).to.eql(localDb.encryptData(constants.TEST_REDIS_TLS_CA)); + + const clientPair: any = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)) + .findOneBy({ id: body.clientCert.id }); + + expect(clientPair.certificate).to.eql(localDb.encryptData(constants.TEST_USER_TLS_CERT)); + expect(clientPair.key).to.eql(localDb.encryptData(constants.TEST_USER_TLS_KEY)); + }, + }); + + // remember certificates ids + existingCACertId = body.caCert.id; + existingClientCertId = body.clientCert.id; + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + it('Should update standalone instance with existing certificates', async () => { + const dbName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint, + data: { + tls: true, + verifyServerCert: true, + caCert: { + id: existingCACertId, + }, + clientCert: { + id: existingClientCertId, + }, + }, + responseSchema, + responseBody: { + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: true, + tlsServername: null, + }, + checkFn: async ({ body }) => { + expect(body.caCert.name).to.be.a('string'); + expect(body.caCert.id).to.eq(existingCACertId); + expect(body.caCert.certificate).to.be.undefined; + + expect(body.clientCert.id).to.deep.eq(existingClientCertId); + expect(body.clientCert.name).to.be.a('string'); + expect(body.clientCert.certificate).to.be.undefined; + expect(body.clientCert.key).to.be.undefined; + }, + }); + }); + it('Should throw an error if try to create client certificate with existing name', async () => { + const dbName = constants.getRandomString(); + const newCaName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint, + statusCode: 400, + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: newCaName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + clientCert: { + name: existingClientCertName, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + }, + }, + responseBody: { + error: 'Bad Request', + message: 'This client certificate name is already in use.', + statusCode: 400, + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + }); + it('Should throw an error if try to create ca certificate with existing name', async () => { + const dbName = constants.getRandomString(); + const newClientName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint, + statusCode: 400, + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: existingCACertName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + clientCert: { + name: newClientName, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + }, + }, + responseBody: { + error: 'Bad Request', + message: 'This ca certificate name is already in use.', + statusCode: 400, + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + }); + }); + }); + describe('CLUSTER', () => { + requirements('rte.type=CLUSTER'); + describe('NO AUTH', function () { + requirements('!rte.tls', '!rte.pass'); + it('Update instance without pass', async () => { + const dbName = constants.getRandomString(); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + ...baseDatabaseData, + name: dbName, + }, + responseSchema, + responseBody: { + name: dbName, + port: constants.TEST_REDIS_PORT, + connectionType: constants.CLUSTER, + nodes: rte.env.nodes, + }, + }); + }); + it('Should throw an error if db index specified', async () => { + const dbName = constants.getRandomString(); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + statusCode: 400, + data: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + db: constants.TEST_REDIS_DB_INDEX + }, + }); + }); + }); + describe('TLS CA', function () { + requirements('rte.tls', '!rte.tlsAuth'); + it('Should create instance without CA tls', async () => { + const dbName = constants.getRandomString(); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: false, + }, + responseSchema, + responseBody: { + name: dbName, + connectionType: constants.CLUSTER, + tls: true, + nodes: rte.env.nodes, + verifyServerCert: false, + }, + }); + }); + it('Should create instance tls and create new CA cert', async () => { + const dbName = constants.getRandomString(); + + const { body } = await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: constants.getRandomString(), + certificate: constants.TEST_REDIS_TLS_CA, + }, + }, + responseSchema, + responseBody: { + name: dbName, + port: constants.TEST_REDIS_PORT, + connectionType: constants.CLUSTER, + nodes: rte.env.nodes, + tls: true, + verifyServerCert: true, + }, + }); + }); + // todo: Should throw an error without CA cert when cert validation enabled + // todo: Should throw an error with invalid CA cert + }); + }); +}); diff --git a/redisinsight/api/test/api/database/PUT-databases-id.test.ts b/redisinsight/api/test/api/database/PUT-databases-id.test.ts index 95a09152cf..d53231eafd 100644 --- a/redisinsight/api/test/api/database/PUT-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PUT-databases-id.test.ts @@ -91,27 +91,7 @@ describe(`PUT /databases/:id`, () => { ].map(mainCheckFn); }); describe('Common', () => { - const newName = constants.getRandomString(); - [ - { - name: 'Should change name (only) for existing database', - data: { - name: newName, - }, - responseSchema, - before: async () => { - oldDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); - expect(oldDatabase.name).to.not.eq(newName); - }, - responseBody: { - name: newName, - }, - after: async () => { - newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); - expect(newDatabase.name).to.eq(newName); - }, - }, { name: 'Should return 503 error if incorrect connection data provided', data: { diff --git a/redisinsight/api/test/helpers/local-db.ts b/redisinsight/api/test/helpers/local-db.ts index 0a7ae6b936..c7c24bddfe 100644 --- a/redisinsight/api/test/helpers/local-db.ts +++ b/redisinsight/api/test/helpers/local-db.ts @@ -292,7 +292,8 @@ export const createDatabaseInstances = async () => { host: 'localhost', port: 3679, connectionType: 'STANDALONE', - ...instance + ...instance, + modules: '[]', }); } } diff --git a/redisinsight/ui/src/slices/instances/instances.ts b/redisinsight/ui/src/slices/instances/instances.ts index 7fc52c2789..36f0bf7d9e 100644 --- a/redisinsight/ui/src/slices/instances/instances.ts +++ b/redisinsight/ui/src/slices/instances/instances.ts @@ -486,7 +486,7 @@ export function changeInstanceAliasAction( const { CancelToken } = axios sourceInstance = CancelToken.source() - const { status } = await apiService.put( + const { status } = await apiService.patch( `${ApiEndpoints.DATABASES}/${id}`, { name }, { cancelToken: sourceInstance.token } diff --git a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts index 24a5cff3dc..769c3d1f36 100644 --- a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts @@ -1023,7 +1023,7 @@ describe('instances slice', () => { newName: 'newAlias', } const responsePayload = { status: 200, data } - apiService.put = jest.fn().mockResolvedValue(responsePayload) + apiService.patch = jest.fn().mockResolvedValue(responsePayload) // Act await store.dispatch( @@ -1047,7 +1047,7 @@ describe('instances slice', () => { data: { message: errorMessage }, }, } - apiService.put = jest.fn().mockRejectedValue(responsePayload) + apiService.patch = jest.fn().mockRejectedValue(responsePayload) // Act await store.dispatch(