Skip to content

Commit a04a0ca

Browse files
Merge pull request #3869 from RedisInsight/bugfix/be/RI-6136
[RI-6136] Disconnect clients for the database after certificate removal
2 parents 936cd9e + fe9f51e commit a04a0ca

11 files changed

+167
-43
lines changed

redisinsight/api/src/__mocks__/common.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type MockType<T> = {
99

1010
export const mockQueryBuilderWhere = jest.fn().mockReturnThis();
1111
export const mockQueryBuilderSelect = jest.fn().mockReturnThis();
12+
export const mockQueryBuilderLeftJoinAndSelect = jest.fn().mockReturnThis();
1213
export const mockQueryBuilderGetOne = jest.fn();
1314
export const mockQueryBuilderGetMany = jest.fn();
1415
export const mockQueryBuilderGetManyRaw = jest.fn();
@@ -26,7 +27,7 @@ export const mockCreateQueryBuilder = jest.fn(() => ({
2627
having: jest.fn().mockReturnThis(),
2728
limit: jest.fn().mockReturnThis(),
2829
leftJoin: jest.fn().mockReturnThis(),
29-
leftJoinAndSelect: jest.fn().mockReturnThis(),
30+
leftJoinAndSelect: mockQueryBuilderLeftJoinAndSelect,
3031
offset: jest.fn().mockReturnThis(),
3132
delete: jest.fn().mockReturnThis(),
3233
whereInIds: jest.fn().mockReturnThis(),

redisinsight/api/src/modules/certificate/ca-certificate.service.spec.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
mockCaCertificate,
66
mockCaCertificateRepository, mockCreateCaCertificateDto,
77
MockType,
8+
mockRedisClientStorage,
89
} from 'src/__mocks__';
910
import { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';
11+
import { RedisClientStorage } from 'src/modules/redis/redis.client.storage';
1012
import { pick } from 'lodash';
1113
import { KeytarEncryptionErrorException } from 'src/modules/encryption/exceptions';
1214
import { CaCertificateService } from './ca-certificate.service';
@@ -24,6 +26,10 @@ describe('CaCertificateService', () => {
2426
provide: CaCertificateRepository,
2527
useFactory: mockCaCertificateRepository,
2628
},
29+
{
30+
provide: RedisClientStorage,
31+
useFactory: mockRedisClientStorage,
32+
},
2733
],
2834
}).compile();
2935

@@ -87,9 +93,26 @@ describe('CaCertificateService', () => {
8793
});
8894

8995
describe('delete', () => {
90-
it('should delete ca certificate', async () => {
91-
expect(await service.delete(mockCaCertificate.id)).toEqual(undefined);
96+
const mockId = 'mock-ca-cert-id';
97+
const mockAffectedDatabases = ['db1', 'db2'];
98+
99+
beforeEach(() => {
100+
jest.clearAllMocks();
101+
});
102+
103+
it('should delete CA certificate and remove affected database clients', async () => {
104+
jest.spyOn(repository, 'delete').mockResolvedValue({ affectedDatabases: mockAffectedDatabases });
105+
jest.spyOn(service['redisClientStorage'], 'removeManyByMetadata').mockResolvedValue(undefined);
106+
107+
await service.delete(mockId);
108+
109+
expect(repository.delete).toHaveBeenCalledWith(mockId);
110+
expect(service['redisClientStorage'].removeManyByMetadata).toHaveBeenCalledTimes(mockAffectedDatabases.length);
111+
mockAffectedDatabases.forEach((databaseId) => {
112+
expect(service['redisClientStorage'].removeManyByMetadata).toHaveBeenCalledWith({ databaseId });
113+
});
92114
});
115+
93116
it('should throw encryption error', async () => {
94117
repository.delete.mockRejectedValueOnce(new KeytarEncryptionErrorException());
95118

redisinsight/api/src/modules/certificate/ca-certificate.service.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import { CaCertificateRepository } from 'src/modules/certificate/repositories/ca
1212
import { CaCertificate } from 'src/modules/certificate/models/ca-certificate';
1313
import { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';
1414
import { classToClass } from 'src/utils';
15+
import { RedisClientStorage } from 'src/modules/redis/redis.client.storage';
1516

1617
@Injectable()
1718
export class CaCertificateService {
1819
private logger = new Logger('CaCertificateService');
1920

2021
constructor(
2122
private readonly repository: CaCertificateRepository,
23+
private redisClientStorage: RedisClientStorage,
2224
) {}
2325

2426
async get(id: string): Promise<CaCertificate> {
@@ -56,10 +58,13 @@ export class CaCertificateService {
5658
}
5759

5860
async delete(id: string): Promise<void> {
59-
this.logger.log(`Deleting certificate. id: ${id}`);
60-
6161
try {
62-
await this.repository.delete(id);
62+
const { affectedDatabases } = await this.repository.delete(id);
63+
64+
await Promise.all(affectedDatabases.map(async (databaseId) => {
65+
// If the certificate is used by the database, remove the client
66+
await this.redisClientStorage.removeManyByMetadata({ databaseId });
67+
}));
6368
} catch (error) {
6469
this.logger.error(`Failed to delete certificate ${id}`, error);
6570

redisinsight/api/src/modules/certificate/client-certificate.service.spec.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import {
66
mockClientCertificate,
77
mockClientCertificateRepository, mockCreateClientCertificateDto,
88
MockType,
9+
mockRedisClientStorage,
910
} from 'src/__mocks__';
1011
import { KeytarEncryptionErrorException } from 'src/modules/encryption/exceptions';
1112
import { ClientCertificateService } from 'src/modules/certificate/client-certificate.service';
1213
import { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';
14+
import { RedisClientStorage } from 'src/modules/redis/redis.client.storage';
1315

1416
describe('ClientCertificateService', () => {
1517
let service: ClientCertificateService;
@@ -24,6 +26,10 @@ describe('ClientCertificateService', () => {
2426
provide: ClientCertificateRepository,
2527
useFactory: mockClientCertificateRepository,
2628
},
29+
{
30+
provide: RedisClientStorage,
31+
useFactory: mockRedisClientStorage,
32+
},
2733
],
2834
}).compile();
2935

@@ -87,9 +93,26 @@ describe('ClientCertificateService', () => {
8793
});
8894

8995
describe('delete', () => {
90-
it('should delete client certificate', async () => {
91-
expect(await service.delete(mockClientCertificate.id)).toEqual(undefined);
96+
const mockId = 'mock-client-cert-id';
97+
const mockAffectedDatabases = ['db1', 'db2'];
98+
99+
beforeEach(() => {
100+
jest.clearAllMocks();
101+
});
102+
103+
it('should delete client certificate and remove affected database clients', async () => {
104+
jest.spyOn(repository, 'delete').mockResolvedValue({ affectedDatabases: mockAffectedDatabases });
105+
jest.spyOn(service['redisClientStorage'], 'removeManyByMetadata').mockResolvedValue(undefined);
106+
107+
await service.delete(mockId);
108+
109+
expect(repository.delete).toHaveBeenCalledWith(mockId);
110+
expect(service['redisClientStorage'].removeManyByMetadata).toHaveBeenCalledTimes(mockAffectedDatabases.length);
111+
mockAffectedDatabases.forEach((databaseId) => {
112+
expect(service['redisClientStorage'].removeManyByMetadata).toHaveBeenCalledWith({ databaseId });
113+
});
92114
});
115+
93116
it('should throw encryption error', async () => {
94117
repository.delete.mockRejectedValueOnce(new KeytarEncryptionErrorException());
95118

redisinsight/api/src/modules/certificate/client-certificate.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import { ClientCertificateRepository } from 'src/modules/certificate/repositorie
1212
import { ClientCertificate } from 'src/modules/certificate/models/client-certificate';
1313
import { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';
1414
import { classToClass } from 'src/utils';
15+
import { RedisClientStorage } from 'src/modules/redis/redis.client.storage';
1516

1617
@Injectable()
1718
export class ClientCertificateService {
1819
private logger = new Logger('ClientCertificateService');
1920

2021
constructor(
2122
private readonly repository: ClientCertificateRepository,
23+
private redisClientStorage: RedisClientStorage,
2224
) {}
2325

2426
/**
@@ -67,7 +69,12 @@ export class ClientCertificateService {
6769
this.logger.log(`Deleting client certificate. id: ${id}`);
6870

6971
try {
70-
await this.repository.delete(id);
72+
const { affectedDatabases } = await this.repository.delete(id);
73+
74+
await Promise.all(affectedDatabases.map(async (databaseId) => {
75+
// If the certificate is used by the database, remove the client
76+
await this.redisClientStorage.removeManyByMetadata({ databaseId });
77+
}));
7178
} catch (error) {
7279
this.logger.error(`Failed to delete certificate ${id}`, error);
7380

redisinsight/api/src/modules/certificate/repositories/ca-certificate.repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,5 @@ export abstract class CaCertificateRepository {
2626
* @param id
2727
* @throws NotFoundException in case when try to delete not existing cert (?)
2828
*/
29-
abstract delete(id: string): Promise<void>;
29+
abstract delete(id: string): Promise<{ affectedDatabases: string[] }>;
3030
}

redisinsight/api/src/modules/certificate/repositories/client-certificate.repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ export abstract class ClientCertificateRepository {
2424
* @param id
2525
* @throws NotFoundException in case when try to delete not existing cert (?)
2626
*/
27-
abstract delete(id: string): Promise<void>;
27+
abstract delete(id: string): Promise<{ affectedDatabases: string[] }>;
2828
}

redisinsight/api/src/modules/certificate/repositories/local.ca-certificate.repository.spec.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { getRepositoryToken } from '@nestjs/typeorm';
55
import { Repository } from 'typeorm';
66
import {
77
mockCaCertificate, mockCaCertificateCertificateEncrypted, mockCaCertificateCertificatePlain, mockCaCertificateEntity,
8-
mockCaCertificateId, mockEncryptionService,
8+
mockCaCertificateId,
9+
mockEncryptionService,
910
mockRepository,
1011
MockType,
1112
} from 'src/__mocks__';
@@ -14,11 +15,13 @@ import { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certifi
1415
import { EncryptionService } from 'src/modules/encryption/encryption.service';
1516
import { BadRequestException, NotFoundException } from '@nestjs/common';
1617
import ERROR_MESSAGES from 'src/constants/error-messages';
18+
import { DatabaseEntity } from 'src/modules/database/entities/database.entity';
1719

1820
describe('LocalCaCertificateRepository', () => {
1921
let service: LocalCaCertificateRepository;
2022
let encryptionService: MockType<EncryptionService>;
2123
let repository: MockType<Repository<CaCertificateEntity>>;
24+
let databaseRepository: MockType<Repository<DatabaseEntity>>;
2225

2326
beforeEach(async () => {
2427
jest.clearAllMocks();
@@ -30,6 +33,10 @@ describe('LocalCaCertificateRepository', () => {
3033
provide: getRepositoryToken(CaCertificateEntity),
3134
useFactory: mockRepository,
3235
},
36+
{
37+
provide: getRepositoryToken(DatabaseEntity),
38+
useFactory: mockRepository,
39+
},
3340
{
3441
provide: EncryptionService,
3542
useFactory: mockEncryptionService,
@@ -38,6 +45,7 @@ describe('LocalCaCertificateRepository', () => {
3845
}).compile();
3946

4047
repository = await module.get(getRepositoryToken(CaCertificateEntity));
48+
databaseRepository = await module.get(getRepositoryToken(DatabaseEntity));
4149
encryptionService = await module.get(EncryptionService);
4250
service = await module.get(LocalCaCertificateRepository);
4351

@@ -100,24 +108,37 @@ describe('LocalCaCertificateRepository', () => {
100108
});
101109

102110
describe('delete', () => {
103-
it('should delete ca certificate', async () => {
104-
const result = await service.delete(mockCaCertificate.id);
105-
106-
expect(result).toEqual(undefined);
111+
it('should delete ca certificate and return affected databases', async () => {
112+
const mockId = 'mock-ca-cert-id';
113+
const mockAffectedDatabases = ['db1', 'db2'];
114+
115+
// Mock findOneBy to return a certificate
116+
repository.findOneBy.mockResolvedValue(mockCaCertificate);
117+
databaseRepository.createQueryBuilder().getMany.mockResolvedValue(mockAffectedDatabases.map((id) => ({ id })));
118+
119+
// Mock delete operation
120+
repository.delete.mockResolvedValue(undefined);
121+
122+
const result = await service.delete(mockId);
123+
124+
expect(result).toEqual({ affectedDatabases: mockAffectedDatabases });
125+
expect(repository.findOneBy).toHaveBeenCalledWith({ id: mockId });
126+
expect(databaseRepository.createQueryBuilder).toHaveBeenCalledWith('d');
127+
expect(databaseRepository.createQueryBuilder().leftJoinAndSelect).toHaveBeenCalledWith('d.caCert', 'c');
128+
expect(databaseRepository.createQueryBuilder().where).toHaveBeenCalledWith({ caCert: mockId });
129+
expect(databaseRepository.createQueryBuilder().select).toHaveBeenCalledWith(['d.id']);
130+
expect(repository.delete).toHaveBeenCalledWith(mockId);
107131
});
108132

109-
it('should throw an error when trying to delete non-existing ca certificate', async () => {
110-
repository.findOneBy.mockResolvedValueOnce(null);
133+
it('should throw NotFoundException when trying to delete non-existing ca certificate', async () => {
134+
const mockId = 'non-existent-id';
111135

112-
try {
113-
await service.delete(mockCaCertificate.id);
114-
fail();
115-
} catch (e) {
116-
expect(e).toBeInstanceOf(NotFoundException);
117-
// todo: why such message?
118-
expect(e.message).toEqual('Not Found');
119-
expect(repository.delete).not.toHaveBeenCalled();
120-
}
136+
// Mock findOneBy to return null (certificate not found)
137+
repository.findOneBy.mockResolvedValue(null);
138+
139+
await expect(service.delete(mockId)).rejects.toThrow(NotFoundException);
140+
expect(repository.findOneBy).toHaveBeenCalledWith({ id: mockId });
141+
expect(repository.delete).not.toHaveBeenCalled();
121142
});
122143
});
123144
});

redisinsight/api/src/modules/certificate/repositories/local.ca-certificate.repository.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ERROR_MESSAGES from 'src/constants/error-messages';
1313
import { classToClass } from 'src/utils';
1414
import { EncryptionService } from 'src/modules/encryption/encryption.service';
1515
import { ModelEncryptor } from 'src/modules/encryption/model.encryptor';
16+
import { DatabaseEntity } from 'src/modules/database/entities/database.entity';
1617

1718
@Injectable()
1819
export class LocalCaCertificateRepository extends CaCertificateRepository {
@@ -23,6 +24,8 @@ export class LocalCaCertificateRepository extends CaCertificateRepository {
2324
constructor(
2425
@InjectRepository(CaCertificateEntity)
2526
private readonly repository: Repository<CaCertificateEntity>,
27+
@InjectRepository(DatabaseEntity)
28+
private readonly databaseRepository: Repository<DatabaseEntity>,
2629
private readonly encryptionService: EncryptionService,
2730
) {
2831
super();
@@ -71,7 +74,7 @@ export class LocalCaCertificateRepository extends CaCertificateRepository {
7174
/**
7275
* @inheritDoc
7376
*/
74-
async delete(id: string): Promise<void> {
77+
async delete(id: string): Promise<{ affectedDatabases: string[] }> {
7578
this.logger.log(`Deleting certificate. id: ${id}`);
7679

7780
// todo: 1. why we need to check if entity exists?
@@ -84,7 +87,16 @@ export class LocalCaCertificateRepository extends CaCertificateRepository {
8487
throw new NotFoundException();
8588
}
8689

90+
const affectedDatabases = (await this.databaseRepository
91+
.createQueryBuilder('d')
92+
.leftJoinAndSelect('d.caCert', 'c')
93+
.where({ caCert: id })
94+
.select(['d.id'])
95+
.getMany()).map((e) => e.id);
96+
8797
await this.repository.delete(id);
8898
this.logger.log(`Succeed to delete ca certificate: ${id}`);
99+
100+
return { affectedDatabases };
89101
}
90102
}

0 commit comments

Comments
 (0)