Skip to content

Commit 22ec264

Browse files
author
arthosofteq
authored
Merge pull request #1595 from RedisInsight/be/feature/RI-3974_ssh_tunnel
#RI-3974 UTests
2 parents f0776b5 + e857fa6 commit 22ec264

38 files changed

+1086
-30
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ aliases:
8888
- mods-preview # OSS Standalone and all preview modules
8989
- oss-st-6-tls # OSS Standalone v6 with TLS enabled
9090
- oss-st-6-tls-auth # OSS Standalone v6 with TLS auth required
91+
- oss-st-6-tls-auth-ssh # OSS Standalone v6 with TLS auth required through ssh
9192
- oss-clu # OSS Cluster
9293
- oss-clu-tls # OSS Cluster with TLS enabled
9394
- oss-sent # OSS Sentinel

redisinsight/api/config/default.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default {
5555
staticContent: !!process.env.SERVER_STATIC_CONTENT || false,
5656
buildType: process.env.BUILD_TYPE || 'ELECTRON',
5757
appVersion: process.env.APP_VERSION || '2.0.0',
58-
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 10000,
58+
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 25000,
5959
excludeRoutes: [],
6060
excludeAuthRoutes: [],
6161
},

redisinsight/api/src/__mocks__/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const mockRepository = jest.fn(() => ({
5454
findOneBy: jest.fn(),
5555
find: jest.fn(),
5656
findByIds: jest.fn(),
57+
merge: jest.fn(),
5758
create: jest.fn(),
5859
save: jest.fn(),
5960
insert: jest.fn(),

redisinsight/api/src/__mocks__/databases.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { pick } from 'lodash';
99
import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
1010
import { DatabaseOverview } from 'src/modules/database/models/database-overview';
1111
import { ClientContext, ClientMetadata } from 'src/common/models';
12+
import {
13+
mockSshOptionsBasic,
14+
mockSshOptionsBasicEntity,
15+
mockSshOptionsPrivateKey,
16+
mockSshOptionsPrivateKeyEntity,
17+
} from 'src/__mocks__/ssh';
1218

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

@@ -34,6 +40,29 @@ export const mockDatabaseEntity = Object.assign(new DatabaseEntity(), {
3440
encryption: null,
3541
});
3642

43+
export const mockDatabaseWithSshBasic = Object.assign(new Database(), {
44+
...mockDatabase,
45+
ssh: true,
46+
sshOptions: mockSshOptionsBasic,
47+
});
48+
49+
export const mockDatabaseWithSshBasicEntity = Object.assign(new DatabaseEntity(), {
50+
...mockDatabaseWithSshBasic,
51+
encryption: null,
52+
sshOptions: mockSshOptionsBasicEntity,
53+
});
54+
55+
export const mockDatabaseWithSshPrivateKey = Object.assign(new Database(), {
56+
...mockDatabase,
57+
ssh: true,
58+
sshOptions: mockSshOptionsPrivateKey,
59+
});
60+
61+
export const mockDatabaseWithSshPrivateKeyEntity = Object.assign(new DatabaseEntity(), {
62+
...mockDatabaseWithSshPrivateKey,
63+
sshOptions: mockSshOptionsPrivateKeyEntity,
64+
});
65+
3766
export const mockDatabaseWithAuth = Object.assign(new Database(), {
3867
...mockDatabase,
3968
username: 'some username',
@@ -181,6 +210,7 @@ export const mockDatabaseService = jest.fn(() => ({
181210

182211
export const mockDatabaseConnectionService = jest.fn(() => ({
183212
getOrCreateClient: jest.fn().mockResolvedValue(mockIORedisClient),
213+
createClient: jest.fn().mockResolvedValue(mockIORedisClient),
184214
}));
185215

186216
export const mockDatabaseInfoProvider = jest.fn(() => ({

redisinsight/api/src/__mocks__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export * from './redis-enterprise';
1818
export * from './redis-sentinel';
1919
export * from './database-import';
2020
export * from './redis-client';
21+
export * from './ssh';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { EncryptionStrategy } from 'src/modules/encryption/models';
2+
import { SshOptions } from 'src/modules/ssh/models/ssh-options';
3+
import { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';
4+
5+
export const mockSshOptionsId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ssh-id';
6+
7+
export const mockSshOptionsUsernamePlain = 'ssh-username';
8+
export const mockSshOptionsUsernameEncrypted = 'ssh.username.ENCRYPTED';
9+
export const mockSshOptionsPasswordPlain = 'ssh-password';
10+
export const mockSshOptionsPasswordEncrypted = 'ssh.password.ENCRYPTED';
11+
export const mockSshOptionsPrivateKeyPlain = '-----BEGIN OPENSSH PRIVATE KEY-----\nssh-private-key';
12+
export const mockSshOptionsPrivateKeyEncrypted = 'ssh.privateKey.ENCRYPTED';
13+
export const mockSshOptionsPassphrasePlain = 'ssh-passphrase';
14+
export const mockSshOptionsPassphraseEncrypted = 'ssh.passphrase.ENCRYPTED';
15+
16+
export const mockSshOptionsBasic = Object.assign(new SshOptions(), {
17+
id: mockSshOptionsId,
18+
host: 'ssh.host.test',
19+
port: 22,
20+
username: mockSshOptionsUsernamePlain,
21+
password: mockSshOptionsPasswordPlain,
22+
privateKey: null,
23+
passphrase: null,
24+
});
25+
26+
export const mockSshOptionsBasicEntity = Object.assign(new SshOptionsEntity(), {
27+
...mockSshOptionsBasic,
28+
username: mockSshOptionsUsernameEncrypted,
29+
password: mockSshOptionsPasswordEncrypted,
30+
encryption: EncryptionStrategy.KEYTAR,
31+
});
32+
33+
export const mockSshOptionsPrivateKey = Object.assign(new SshOptions(), {
34+
...mockSshOptionsBasic,
35+
password: null,
36+
privateKey: mockSshOptionsPrivateKeyPlain,
37+
passphrase: mockSshOptionsPassphrasePlain,
38+
});
39+
40+
export const mockSshOptionsPrivateKeyEntity = Object.assign(new SshOptionsEntity(), {
41+
...mockSshOptionsBasicEntity,
42+
password: null,
43+
privateKey: mockSshOptionsPrivateKeyEncrypted,
44+
passphrase: mockSshOptionsPassphraseEncrypted,
45+
});
46+
47+
export const mockSshTunnelProvider = jest.fn(() => {
48+
49+
});

redisinsight/api/src/modules/database/database.analytics.spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,11 @@ describe('DatabaseAnalytics', () => {
7979
});
8080

8181
describe('sendInstanceAddedEvent', () => {
82-
it('should emit event with enabled tls and sni', () => {
83-
service.sendInstanceAddedEvent(mockDatabaseWithTlsAuth, mockRedisGeneralInfo);
82+
it('should emit event with enabled tls and sni, and ssh', () => {
83+
service.sendInstanceAddedEvent({
84+
...mockDatabaseWithTlsAuth,
85+
ssh: true,
86+
}, mockRedisGeneralInfo);
8487

8588
expect(sendEventSpy).toHaveBeenCalledWith(
8689
TelemetryEvents.RedisInstanceAdded,
@@ -92,6 +95,7 @@ describe('DatabaseAnalytics', () => {
9295
verifyTLSCertificate: 'enabled',
9396
useTLSAuthClients: 'enabled',
9497
useSNI: 'enabled',
98+
useSSH: 'enabled',
9599
version: mockRedisGeneralInfo.version,
96100
numberOfKeys: mockRedisGeneralInfo.totalKeys,
97101
numberOfKeysRange: '0 - 500 000',
@@ -119,6 +123,7 @@ describe('DatabaseAnalytics', () => {
119123
verifyTLSCertificate: 'disabled',
120124
useTLSAuthClients: 'disabled',
121125
useSNI: 'disabled',
126+
useSSH: 'disabled',
122127
version: mockRedisGeneralInfo.version,
123128
numberOfKeys: mockRedisGeneralInfo.totalKeys,
124129
numberOfKeysRange: '0 - 500 000',
@@ -148,6 +153,7 @@ describe('DatabaseAnalytics', () => {
148153
verifyTLSCertificate: 'enabled',
149154
useTLSAuthClients: 'enabled',
150155
useSNI: 'enabled',
156+
useSSH: 'disabled',
151157
version: mockRedisGeneralInfo.version,
152158
numberOfKeys: undefined,
153159
numberOfKeysRange: undefined,

redisinsight/api/src/modules/database/database.analytics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export class DatabaseAnalytics extends TelemetryBaseService {
6060
useSNI: instance?.tlsServername
6161
? 'enabled'
6262
: 'disabled',
63+
useSSH: instance?.ssh ? 'enabled' : 'disabled',
6364
version: additionalInfo?.version,
6465
numberOfKeys: additionalInfo?.totalKeys,
6566
numberOfKeysRange: getRangeForNumber(additionalInfo?.totalKeys, TOTAL_KEYS_BREAKPOINTS),

redisinsight/api/src/modules/database/repositories/local.database.repository.spec.ts

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,37 @@ import { getRepositoryToken } from '@nestjs/typeorm';
55
import { Repository } from 'typeorm';
66
import {
77
mockCaCertificateRepository,
8-
mockClientCertificateRepository, mockClusterDatabaseWithTlsAuth, mockClusterDatabaseWithTlsAuthEntity,
8+
mockClientCertificateRepository,
9+
mockClusterDatabaseWithTlsAuth,
10+
mockClusterDatabaseWithTlsAuthEntity,
911
mockDatabase,
1012
mockDatabaseEntity,
1113
mockDatabaseId,
1214
mockDatabasePasswordEncrypted,
1315
mockDatabasePasswordPlain,
1416
mockDatabaseSentinelMasterPasswordEncrypted,
1517
mockDatabaseSentinelMasterPasswordPlain,
16-
mockDatabaseWithTls, mockDatabaseWithTlsAuth,
18+
mockDatabaseWithSshBasic,
19+
mockDatabaseWithSshBasicEntity,
20+
mockDatabaseWithSshPrivateKey,
21+
mockDatabaseWithSshPrivateKeyEntity,
22+
mockDatabaseWithTls,
23+
mockDatabaseWithTlsAuth,
1724
mockDatabaseWithTlsAuthEntity,
1825
mockDatabaseWithTlsEntity,
1926
mockEncryptionService,
20-
mockRepository, mockSentinelDatabaseWithTlsAuth, mockSentinelDatabaseWithTlsAuthEntity,
27+
mockRepository,
28+
mockSentinelDatabaseWithTlsAuth,
29+
mockSentinelDatabaseWithTlsAuthEntity,
30+
mockSshOptionsBasicEntity,
31+
mockSshOptionsPassphraseEncrypted,
32+
mockSshOptionsPassphrasePlain,
33+
mockSshOptionsPasswordEncrypted,
34+
mockSshOptionsPasswordPlain,
35+
mockSshOptionsPrivateKeyEncrypted, mockSshOptionsPrivateKeyEntity,
36+
mockSshOptionsPrivateKeyPlain,
37+
mockSshOptionsUsernameEncrypted,
38+
mockSshOptionsUsernamePlain,
2139
MockType,
2240
} from 'src/__mocks__';
2341
import { EncryptionService } from 'src/modules/encryption/encryption.service';
@@ -26,6 +44,7 @@ import { DatabaseEntity } from 'src/modules/database/entities/database.entity';
2644
import { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';
2745
import { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';
2846
import { cloneClassInstance } from 'src/utils';
47+
import { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';
2948

3049
const listFields = [
3150
'id', 'name', 'host', 'port', 'db',
@@ -36,6 +55,7 @@ describe('LocalDatabaseRepository', () => {
3655
let service: LocalDatabaseRepository;
3756
let encryptionService: MockType<EncryptionService>;
3857
let repository: MockType<Repository<DatabaseEntity>>;
58+
let sshOptionsRepository: MockType<Repository<SshOptionsEntity>>;
3959
let caCertRepository: MockType<CaCertificateRepository>;
4060
let clientCertRepository: MockType<ClientCertificateRepository>;
4161

@@ -49,6 +69,10 @@ describe('LocalDatabaseRepository', () => {
4969
provide: getRepositoryToken(DatabaseEntity),
5070
useFactory: mockRepository,
5171
},
72+
{
73+
provide: getRepositoryToken(SshOptionsEntity),
74+
useFactory: mockRepository,
75+
},
5276
{
5377
provide: EncryptionService,
5478
useFactory: mockEncryptionService,
@@ -65,6 +89,7 @@ describe('LocalDatabaseRepository', () => {
6589
}).compile();
6690

6791
repository = await module.get(getRepositoryToken(DatabaseEntity));
92+
sshOptionsRepository = await module.get(getRepositoryToken(SshOptionsEntity));
6893
caCertRepository = await module.get(CaCertificateRepository);
6994
clientCertRepository = await module.get(ClientCertificateRepository);
7095
encryptionService = await module.get(EncryptionService);
@@ -83,7 +108,15 @@ describe('LocalDatabaseRepository', () => {
83108
.calledWith(mockDatabasePasswordEncrypted, jasmine.anything())
84109
.mockResolvedValue(mockDatabasePasswordPlain)
85110
.calledWith(mockDatabaseSentinelMasterPasswordEncrypted, jasmine.anything())
86-
.mockResolvedValue(mockDatabaseSentinelMasterPasswordPlain);
111+
.mockResolvedValue(mockDatabaseSentinelMasterPasswordPlain)
112+
.calledWith(mockSshOptionsUsernameEncrypted, jasmine.anything())
113+
.mockResolvedValue(mockSshOptionsUsernamePlain)
114+
.calledWith(mockSshOptionsPasswordEncrypted, jasmine.anything())
115+
.mockResolvedValue(mockSshOptionsPasswordPlain)
116+
.calledWith(mockSshOptionsPrivateKeyEncrypted, jasmine.anything())
117+
.mockResolvedValue(mockSshOptionsPrivateKeyPlain)
118+
.calledWith(mockSshOptionsPassphraseEncrypted, jasmine.anything())
119+
.mockResolvedValue(mockSshOptionsPassphrasePlain);
87120
when(encryptionService.encrypt)
88121
.calledWith(mockDatabasePasswordPlain)
89122
.mockResolvedValue({
@@ -94,6 +127,26 @@ describe('LocalDatabaseRepository', () => {
94127
.mockResolvedValue({
95128
data: mockDatabaseSentinelMasterPasswordEncrypted,
96129
encryption: mockDatabaseWithTlsAuthEntity.encryption,
130+
})
131+
.calledWith(mockSshOptionsUsernamePlain)
132+
.mockResolvedValue({
133+
data: mockSshOptionsUsernameEncrypted,
134+
encryption: mockSshOptionsBasicEntity.encryption,
135+
})
136+
.calledWith(mockSshOptionsPasswordPlain)
137+
.mockResolvedValue({
138+
data: mockSshOptionsPasswordEncrypted,
139+
encryption: mockSshOptionsBasicEntity.encryption,
140+
})
141+
.calledWith(mockSshOptionsPrivateKeyPlain)
142+
.mockResolvedValue({
143+
data: mockSshOptionsPrivateKeyEncrypted,
144+
encryption: mockSshOptionsPrivateKeyEntity.encryption,
145+
})
146+
.calledWith(mockSshOptionsPassphrasePlain)
147+
.mockResolvedValue({
148+
data: mockSshOptionsPassphraseEncrypted,
149+
encryption: mockSshOptionsPrivateKeyEntity.encryption,
97150
});
98151
});
99152

@@ -117,6 +170,24 @@ describe('LocalDatabaseRepository', () => {
117170
expect(clientCertRepository.get).not.toHaveBeenCalled();
118171
});
119172

173+
it('should return standalone database model with ssh enabled (basic)', async () => {
174+
repository.findOneBy.mockResolvedValue(mockDatabaseWithSshBasicEntity);
175+
const result = await service.get(mockDatabaseWithSshBasic.id);
176+
177+
expect(result).toEqual(mockDatabaseWithSshBasic);
178+
expect(caCertRepository.get).not.toHaveBeenCalled();
179+
expect(clientCertRepository.get).not.toHaveBeenCalled();
180+
});
181+
182+
it('should return standalone database model with ssh enabled (privateKey + passphrase)', async () => {
183+
repository.findOneBy.mockResolvedValue(mockDatabaseWithSshPrivateKeyEntity);
184+
const result = await service.get(mockDatabaseWithSshPrivateKey.id);
185+
186+
expect(result).toEqual(mockDatabaseWithSshPrivateKey);
187+
expect(caCertRepository.get).not.toHaveBeenCalled();
188+
expect(clientCertRepository.get).not.toHaveBeenCalled();
189+
});
190+
120191
it('should return standalone model with ca tls', async () => {
121192
repository.findOneBy.mockResolvedValue(mockDatabaseWithTlsEntity);
122193

@@ -201,14 +272,51 @@ describe('LocalDatabaseRepository', () => {
201272

202273
describe('update', () => {
203274
it('should update standalone database', async () => {
204-
const result = await service.update(mockDatabaseId, mockDatabase);
275+
repository.merge.mockReturnValue(mockDatabaseEntity);
205276

206-
expect(result).toEqual(mockDatabase);
277+
const result = await service.update(mockDatabaseId, {
278+
...mockDatabase,
279+
caCert: null,
280+
clientCert: null,
281+
sshOptions: null,
282+
});
283+
284+
expect(result).toEqual({
285+
...mockDatabase,
286+
caCert: null,
287+
clientCert: null,
288+
sshOptions: null,
289+
});
290+
expect(caCertRepository.create).not.toHaveBeenCalled();
291+
expect(clientCertRepository.create).not.toHaveBeenCalled();
292+
expect(sshOptionsRepository.createQueryBuilder).toHaveBeenCalled();
293+
});
294+
295+
it('should update standalone database with ssh enabled (basic)', async () => {
296+
repository.findOneBy.mockResolvedValue(mockDatabaseWithSshBasicEntity);
297+
repository.merge.mockReturnValue(mockDatabaseWithSshBasic);
298+
299+
const result = await service.update(mockDatabaseId, mockDatabaseWithSshBasic);
300+
301+
expect(result).toEqual(mockDatabaseWithSshBasic);
302+
expect(caCertRepository.create).not.toHaveBeenCalled();
303+
expect(clientCertRepository.create).not.toHaveBeenCalled();
304+
});
305+
306+
it('should update standalone database with ssh enabled (privateKey)', async () => {
307+
repository.findOneBy.mockResolvedValue(mockDatabaseWithSshPrivateKeyEntity);
308+
repository.merge.mockReturnValue(mockDatabaseWithSshPrivateKey);
309+
310+
const result = await service.update(mockDatabaseId, mockDatabaseWithSshPrivateKey);
311+
312+
expect(result).toEqual(mockDatabaseWithSshPrivateKey);
207313
expect(caCertRepository.create).not.toHaveBeenCalled();
208314
expect(clientCertRepository.create).not.toHaveBeenCalled();
209315
});
210316

211317
it('should update standalone database (with existing certificates)', async () => {
318+
repository.merge.mockReturnValue(mockDatabaseWithTlsAuth);
319+
repository.findOneBy.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);
212320
repository.findOneBy.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);
213321

214322
const result = await service.update(mockDatabaseId, mockDatabaseWithTlsAuth);
@@ -219,6 +327,8 @@ describe('LocalDatabaseRepository', () => {
219327
});
220328

221329
it('should update standalone database (and certificates)', async () => {
330+
repository.merge.mockReturnValue(mockDatabaseWithTlsAuth);
331+
repository.findOneBy.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);
222332
repository.findOneBy.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);
223333

224334
const result = await service.update(

0 commit comments

Comments
 (0)