Skip to content

Commit 86128a6

Browse files
authored
Merge pull request #1567 from RedisInsight/be/feature/RI-3974_ssh_tunnel
#RI-3974 ssh tunneling base implementation
2 parents fb4f97d + a3119e9 commit 86128a6

29 files changed

+756
-56
lines changed

redisinsight/api/config/ormconfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SettingsEntity } from 'src/modules/settings/entities/settings.entity';
1010
import { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';
1111
import { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';
1212
import { DatabaseEntity } from 'src/modules/database/entities/database.entity';
13+
import { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';
1314
import migrations from '../migration';
1415
import * as config from '../src/utils/config';
1516

@@ -31,6 +32,7 @@ const ormConfig = {
3132
PluginStateEntity,
3233
NotificationEntity,
3334
DatabaseAnalysisEntity,
35+
SshOptionsEntity,
3436
],
3537
migrations,
3638
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class sshOptions1673035852335 implements MigrationInterface {
4+
name = 'sshOptions1673035852335'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "ssh_options" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "encryption" varchar, "username" varchar, "password" varchar, "privateKey" varchar, "passphrase" varchar, "databaseId" varchar, CONSTRAINT "REL_fe3c3f8b1246e4824a3fb83047" UNIQUE ("databaseId"))`);
8+
await queryRunner.query(`CREATE TABLE "temporary_database_instance" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "name" varchar NOT NULL, "username" varchar, "password" varchar, "tls" boolean, "verifyServerCert" boolean, "lastConnection" datetime, "caCertId" varchar, "clientCertId" varchar, "connectionType" varchar NOT NULL DEFAULT ('STANDALONE'), "nodes" varchar DEFAULT ('[]'), "nameFromProvider" varchar, "sentinelMasterName" varchar, "sentinelMasterUsername" varchar, "sentinelMasterPassword" varchar, "provider" varchar DEFAULT ('UNKNOWN'), "modules" varchar NOT NULL DEFAULT ('[]'), "db" integer, "encryption" varchar, "tlsServername" varchar, "new" boolean, "ssh" boolean, CONSTRAINT "FK_d1bc747b5938e22b4b708d8e9a5" FOREIGN KEY ("caCertId") REFERENCES "ca_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_3b9b625266c00feb2d66a9f36e4" FOREIGN KEY ("clientCertId") REFERENCES "client_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`);
9+
await queryRunner.query(`INSERT INTO "temporary_database_instance"("id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername", "new") SELECT "id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername", "new" FROM "database_instance"`);
10+
await queryRunner.query(`DROP TABLE "database_instance"`);
11+
await queryRunner.query(`ALTER TABLE "temporary_database_instance" RENAME TO "database_instance"`);
12+
await queryRunner.query(`CREATE TABLE "temporary_ssh_options" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "encryption" varchar, "username" varchar, "password" varchar, "privateKey" varchar, "passphrase" varchar, "databaseId" varchar, CONSTRAINT "REL_fe3c3f8b1246e4824a3fb83047" UNIQUE ("databaseId"), CONSTRAINT "FK_fe3c3f8b1246e4824a3fb83047d" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`);
13+
await queryRunner.query(`INSERT INTO "temporary_ssh_options"("id", "host", "port", "encryption", "username", "password", "privateKey", "passphrase", "databaseId") SELECT "id", "host", "port", "encryption", "username", "password", "privateKey", "passphrase", "databaseId" FROM "ssh_options"`);
14+
await queryRunner.query(`DROP TABLE "ssh_options"`);
15+
await queryRunner.query(`ALTER TABLE "temporary_ssh_options" RENAME TO "ssh_options"`);
16+
}
17+
18+
public async down(queryRunner: QueryRunner): Promise<void> {
19+
await queryRunner.query(`ALTER TABLE "ssh_options" RENAME TO "temporary_ssh_options"`);
20+
await queryRunner.query(`CREATE TABLE "ssh_options" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "encryption" varchar, "username" varchar, "password" varchar, "privateKey" varchar, "passphrase" varchar, "databaseId" varchar, CONSTRAINT "REL_fe3c3f8b1246e4824a3fb83047" UNIQUE ("databaseId"))`);
21+
await queryRunner.query(`INSERT INTO "ssh_options"("id", "host", "port", "encryption", "username", "password", "privateKey", "passphrase", "databaseId") SELECT "id", "host", "port", "encryption", "username", "password", "privateKey", "passphrase", "databaseId" FROM "temporary_ssh_options"`);
22+
await queryRunner.query(`DROP TABLE "temporary_ssh_options"`);
23+
await queryRunner.query(`ALTER TABLE "database_instance" RENAME TO "temporary_database_instance"`);
24+
await queryRunner.query(`CREATE TABLE "database_instance" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "name" varchar NOT NULL, "username" varchar, "password" varchar, "tls" boolean, "verifyServerCert" boolean, "lastConnection" datetime, "caCertId" varchar, "clientCertId" varchar, "connectionType" varchar NOT NULL DEFAULT ('STANDALONE'), "nodes" varchar DEFAULT ('[]'), "nameFromProvider" varchar, "sentinelMasterName" varchar, "sentinelMasterUsername" varchar, "sentinelMasterPassword" varchar, "provider" varchar DEFAULT ('UNKNOWN'), "modules" varchar NOT NULL DEFAULT ('[]'), "db" integer, "encryption" varchar, "tlsServername" varchar, "new" boolean, CONSTRAINT "FK_d1bc747b5938e22b4b708d8e9a5" FOREIGN KEY ("caCertId") REFERENCES "ca_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_3b9b625266c00feb2d66a9f36e4" FOREIGN KEY ("clientCertId") REFERENCES "client_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`);
25+
await queryRunner.query(`INSERT INTO "database_instance"("id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername", "new") SELECT "id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername", "new" FROM "temporary_database_instance"`);
26+
await queryRunner.query(`DROP TABLE "temporary_database_instance"`);
27+
await queryRunner.query(`DROP TABLE "ssh_options"`);
28+
}
29+
30+
}

redisinsight/api/migration/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { databaseAnalysisExpirationGroups1664886479051 } from './1664886479051-d
2222
import { workbenchExecutionTime1667368983699 } from './1667368983699-workbench-execution-time';
2323
import { database1667477693934 } from './1667477693934-database';
2424
import { databaseNew1670252337342 } from './1670252337342-database-new';
25+
import { sshOptions1673035852335 } from './1673035852335-ssh-options';
2526

2627
export default [
2728
initialMigration1614164490968,
@@ -48,4 +49,5 @@ export default [
4849
workbenchExecutionTime1667368983699,
4950
database1667477693934,
5051
databaseNew1670252337342,
52+
sshOptions1673035852335,
5153
];

redisinsight/api/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"body-parser": "^1.19.0",
5454
"class-transformer": "^0.2.3",
5555
"class-validator": "^0.12.2",
56+
"detect-port": "^1.5.1",
5657
"dotenv": "^16.0.0",
5758
"express": "^4.17.1",
5859
"fs-extra": "^10.0.0",
@@ -67,6 +68,7 @@
6768
"socket.io": "^4.4.0",
6869
"source-map-support": "^0.5.19",
6970
"sqlite3": "^5.0.11",
71+
"ssh2": "^1.11.0",
7072
"swagger-ui-express": "^4.1.4",
7173
"typeorm": "^0.3.9",
7274
"uuid": "^8.3.2",
@@ -84,6 +86,7 @@
8486
"@types/lodash": "^4.14.167",
8587
"@types/node": "14.14.10",
8688
"@types/socket.io": "^3.0.2",
89+
"@types/ssh2": "^1.11.6",
8790
"@types/supertest": "^2.0.8",
8891
"@typescript-eslint/eslint-plugin": "^4.8.1",
8992
"@typescript-eslint/parser": "^4.8.1",

redisinsight/api/src/core.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CertificateModule } from 'src/modules/certificate/certificate.module';
66
import { EventEmitterModule } from '@nestjs/event-emitter';
77
import { RedisModule } from 'src/modules/redis/redis.module';
88
import { AnalyticsModule } from 'src/modules/analytics/analytics.module';
9+
import { SshModule } from 'src/modules/ssh/ssh.module';
910

1011
@Global()
1112
@Module({
@@ -17,13 +18,15 @@ import { AnalyticsModule } from 'src/modules/analytics/analytics.module';
1718
CertificateModule.register(),
1819
DatabaseModule.register(),
1920
RedisModule,
21+
SshModule,
2022
],
2123
exports: [
2224
EncryptionModule,
2325
SettingsModule,
2426
CertificateModule,
2527
DatabaseModule,
2628
RedisModule,
29+
SshModule,
2730
],
2831
})
2932
export class CoreModule {}

redisinsight/api/src/modules/database/dto/create.database.dto.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ import { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certific
1212
import { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto';
1313
import { caCertTransformer } from 'src/modules/certificate/transformers/ca-cert.transformer';
1414
import { clientCertTransformer } from 'src/modules/certificate/transformers/client-cert.transformer';
15+
import { CreateBasicSshOptionsDto } from 'src/modules/ssh/dto/create.basic-ssh-options.dto';
16+
import { CreateCertSshOptionsDto } from 'src/modules/ssh/dto/create.cert-ssh-options.dto';
17+
import { sshOptionsTransformer } from 'src/modules/ssh/transformers/ssh-options.transformer';
1518

16-
@ApiExtraModels(CreateCaCertificateDto, UseCaCertificateDto, CreateClientCertificateDto, UseClientCertificateDto)
19+
@ApiExtraModels(
20+
CreateCaCertificateDto, UseCaCertificateDto,
21+
CreateClientCertificateDto, UseClientCertificateDto,
22+
CreateBasicSshOptionsDto, CreateCertSshOptionsDto,
23+
)
1724
export class CreateDatabaseDto extends PickType(Database, [
1825
'host', 'port', 'name', 'db', 'username', 'password', 'nameFromProvider', 'provider',
19-
'tls', 'tlsServername', 'verifyServerCert', 'sentinelMaster',
26+
'tls', 'tlsServername', 'verifyServerCert', 'sentinelMaster', 'ssh',
2027
] as const) {
2128
@ApiPropertyOptional({
2229
description: 'CA Certificate',
@@ -45,4 +52,18 @@ export class CreateDatabaseDto extends PickType(Database, [
4552
@Type(clientCertTransformer)
4653
@ValidateNested()
4754
clientCert?: CreateClientCertificateDto | UseClientCertificateDto;
55+
56+
@ApiPropertyOptional({
57+
description: 'SSH Options',
58+
oneOf: [
59+
{ $ref: getSchemaPath(CreateBasicSshOptionsDto) },
60+
{ $ref: getSchemaPath(CreateCertSshOptionsDto) },
61+
],
62+
})
63+
@Expose()
64+
@IsOptional()
65+
@IsNotEmptyObject()
66+
@Type(sshOptionsTransformer)
67+
@ValidateNested()
68+
sshOptions?: CreateBasicSshOptionsDto | CreateCertSshOptionsDto;
4869
}

redisinsight/api/src/modules/database/dto/update.database.dto.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { clientCertTransformer } from 'src/modules/certificate/transformers/clie
1313
import { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto';
1414
import { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';
1515
import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto';
16+
import { CreateBasicSshOptionsDto } from 'src/modules/ssh/dto/create.basic-ssh-options.dto';
17+
import { CreateCertSshOptionsDto } from 'src/modules/ssh/dto/create.cert-ssh-options.dto';
18+
import { sshOptionsTransformer } from 'src/modules/ssh/transformers/ssh-options.transformer';
1619

1720
export class UpdateDatabaseDto extends CreateDatabaseDto {
1821
@ValidateIf((object, value) => value !== undefined)
@@ -28,6 +31,31 @@ export class UpdateDatabaseDto extends CreateDatabaseDto {
2831
@IsInt({ always: true })
2932
port: number;
3033

34+
@ApiPropertyOptional({
35+
description:
36+
'Database username, if your database is ACL enabled, otherwise leave this field empty.',
37+
type: String,
38+
})
39+
@Expose()
40+
@IsString({ always: true })
41+
@IsNotEmpty()
42+
@IsOptional()
43+
@Default(null)
44+
username?: string;
45+
46+
@ApiPropertyOptional({
47+
description:
48+
'The password, if any, for your Redis database. '
49+
+ 'If your database doesn’t require a password, leave this field empty.',
50+
type: String,
51+
})
52+
@Expose()
53+
@IsString({ always: true })
54+
@IsNotEmpty()
55+
@IsOptional()
56+
@Default(null)
57+
password?: string;
58+
3159
@ApiPropertyOptional({
3260
description: 'Logical database number.',
3361
type: Number,
@@ -47,6 +75,15 @@ export class UpdateDatabaseDto extends CreateDatabaseDto {
4775
@Default(false)
4876
tls?: boolean;
4977

78+
@ApiPropertyOptional({
79+
description: 'Use SSH to connect.',
80+
type: Boolean,
81+
})
82+
@IsBoolean()
83+
@IsOptional()
84+
@Default(false)
85+
ssh?: boolean;
86+
5087
@ApiPropertyOptional({
5188
description: 'SNI servername',
5289
type: String,
@@ -105,4 +142,19 @@ export class UpdateDatabaseDto extends CreateDatabaseDto {
105142
@ValidateNested()
106143
@Default(null)
107144
sentinelMaster?: SentinelMaster;
145+
146+
@ApiPropertyOptional({
147+
description: 'SSH Options',
148+
oneOf: [
149+
{ $ref: getSchemaPath(CreateBasicSshOptionsDto) },
150+
{ $ref: getSchemaPath(CreateCertSshOptionsDto) },
151+
],
152+
})
153+
@Expose()
154+
@IsOptional()
155+
@IsNotEmptyObject()
156+
@Type(sshOptionsTransformer)
157+
@ValidateNested()
158+
@Default(null)
159+
sshOptions?: CreateBasicSshOptionsDto | CreateCertSshOptionsDto;
108160
}

redisinsight/api/src/modules/database/entities/database.entity.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {
2-
Column, Entity, ManyToOne, PrimaryGeneratedColumn,
2+
Column, Entity, ManyToOne, OneToOne, PrimaryGeneratedColumn,
33
} from 'typeorm';
44
import { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';
55
import { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';
66
import { DataAsJsonString } from 'src/common/decorators';
7-
import { Expose, Transform } from 'class-transformer';
7+
import { Expose, Transform, Type } from 'class-transformer';
88
import { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';
9+
import { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';
910

1011
export enum HostingProvider {
1112
UNKNOWN = 'UNKNOWN',
@@ -162,4 +163,21 @@ export class DatabaseEntity {
162163
@Expose()
163164
@Column({ nullable: true })
164165
new: boolean;
166+
167+
@Expose()
168+
@Column({ nullable: true })
169+
ssh: boolean;
170+
171+
@Expose()
172+
@OneToOne(
173+
() => SshOptionsEntity,
174+
(sshOptions) => sshOptions.database,
175+
{
176+
eager: true,
177+
onDelete: 'CASCADE',
178+
cascade: true,
179+
},
180+
)
181+
@Type(() => SshOptionsEntity)
182+
sshOptions: SshOptionsEntity;
165183
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';
1818
import { Endpoint } from 'src/common/models';
1919
import { AdditionalRedisModule } from 'src/modules/database/models/additional.redis.module';
20+
import { SshOptions } from 'src/modules/ssh/models/ssh-options';
2021

2122
export class Database {
2223
@ApiProperty({
@@ -215,4 +216,24 @@ export class Database {
215216
@IsOptional()
216217
@IsBoolean({ always: true })
217218
new?: boolean;
219+
220+
@ApiPropertyOptional({
221+
description: 'Use SSH tunnel to connect.',
222+
type: Boolean,
223+
})
224+
@Expose()
225+
@IsBoolean()
226+
@IsOptional()
227+
ssh?: boolean;
228+
229+
@ApiPropertyOptional({
230+
description: 'SSH options',
231+
type: SshOptions,
232+
})
233+
@Expose()
234+
@IsOptional()
235+
@IsNotEmptyObject()
236+
@Type(() => SshOptions)
237+
@ValidateNested()
238+
sshOptions?: SshOptions;
218239
}

0 commit comments

Comments
 (0)