dbgate-api 5.0.6 → 5.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/env/portal/.env +11 -0
- package/package.json +7 -5
- package/src/controllers/config.js +24 -10
- package/src/controllers/connections.js +12 -7
- package/src/controllers/databaseConnections.js +54 -22
- package/src/controllers/serverConnections.js +11 -5
- package/src/currentVersion.js +2 -2
- package/src/main.js +5 -1
- package/src/proc/sshForwardProcess.js +3 -1
- package/src/utility/SSHConnection.js +251 -0
- package/src/utility/connectUtility.js +0 -3
- package/src/utility/hasPermission.js +31 -3
- package/src/utility/socket.js +20 -23
- package/src/utility/useController.js +1 -5
package/env/portal/.env
CHANGED
|
@@ -48,4 +48,15 @@ PASSWORD_relational=relational
|
|
|
48
48
|
ENGINE_relational=mariadb@dbgate-plugin-mysql
|
|
49
49
|
READONLY_relational=1
|
|
50
50
|
|
|
51
|
+
# SETTINGS_dataGrid.showHintColumns=1
|
|
52
|
+
|
|
51
53
|
# docker run -p 3000:3000 -e CONNECTIONS=mongo -e URL_mongo=mongodb://localhost:27017 -e ENGINE_mongo=mongo@dbgate-plugin-mongo -e LABEL_mongo=mongo dbgate/dbgate:beta
|
|
54
|
+
|
|
55
|
+
# LOGINS=x,y
|
|
56
|
+
# LOGIN_PASSWORD_x=x
|
|
57
|
+
# LOGIN_PASSWORD_y=LOGIN_PASSWORD_y
|
|
58
|
+
# LOGIN_PERMISSIONS_x=~*
|
|
59
|
+
# LOGIN_PERMISSIONS_y=~*
|
|
60
|
+
|
|
61
|
+
# PERMISSIONS=~*,connections/relational
|
|
62
|
+
# PERMISSIONS=~*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbgate-api",
|
|
3
3
|
"main": "src/index.js",
|
|
4
|
-
"version": "5.0.
|
|
4
|
+
"version": "5.0.9",
|
|
5
5
|
"homepage": "https://dbgate.org/",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"cors": "^2.8.5",
|
|
27
27
|
"cross-env": "^6.0.3",
|
|
28
28
|
"dbgate-query-splitter": "^4.9.0",
|
|
29
|
-
"dbgate-sqltree": "^5.0.
|
|
30
|
-
"dbgate-tools": "^5.0.
|
|
29
|
+
"dbgate-sqltree": "^5.0.9",
|
|
30
|
+
"dbgate-tools": "^5.0.9",
|
|
31
|
+
"debug": "^4.3.4",
|
|
31
32
|
"diff": "^5.0.0",
|
|
32
33
|
"diff2html": "^3.4.13",
|
|
33
34
|
"eslint": "^6.8.0",
|
|
@@ -45,9 +46,10 @@
|
|
|
45
46
|
"lodash": "^4.17.21",
|
|
46
47
|
"ncp": "^2.0.0",
|
|
47
48
|
"node-cron": "^2.0.3",
|
|
48
|
-
"
|
|
49
|
+
"on-finished": "^2.4.1",
|
|
49
50
|
"portfinder": "^1.0.28",
|
|
50
51
|
"simple-encryptor": "^4.0.0",
|
|
52
|
+
"ssh2": "^1.11.0",
|
|
51
53
|
"tar": "^6.0.5",
|
|
52
54
|
"uuid": "^3.4.0"
|
|
53
55
|
},
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
"devDependencies": {
|
|
64
66
|
"@types/fs-extra": "^9.0.11",
|
|
65
67
|
"@types/lodash": "^4.14.149",
|
|
66
|
-
"dbgate-types": "^5.0.
|
|
68
|
+
"dbgate-types": "^5.0.9",
|
|
67
69
|
"env-cmd": "^10.1.0",
|
|
68
70
|
"node-loader": "^1.0.2",
|
|
69
71
|
"nodemon": "^2.0.2",
|
|
@@ -29,7 +29,7 @@ module.exports = {
|
|
|
29
29
|
async get(_params, req) {
|
|
30
30
|
const logins = getLogins();
|
|
31
31
|
const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null;
|
|
32
|
-
const permissions = login ? login.permissions :
|
|
32
|
+
const permissions = login ? login.permissions : process.env.PERMISSIONS;
|
|
33
33
|
|
|
34
34
|
return {
|
|
35
35
|
runAsPortal: !!connections.portalConnections,
|
|
@@ -59,13 +59,10 @@ module.exports = {
|
|
|
59
59
|
|
|
60
60
|
getSettings_meta: true,
|
|
61
61
|
async getSettings() {
|
|
62
|
-
|
|
63
|
-
return this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} catch (err) {
|
|
67
|
-
return this.fillMissingSettings({});
|
|
68
|
-
}
|
|
62
|
+
const res = await lock.acquire('settings', async () => {
|
|
63
|
+
return await this.loadSettings();
|
|
64
|
+
});
|
|
65
|
+
return res;
|
|
69
66
|
},
|
|
70
67
|
|
|
71
68
|
fillMissingSettings(value) {
|
|
@@ -76,15 +73,32 @@ module.exports = {
|
|
|
76
73
|
// res['app.useNativeMenu'] = os.platform() == 'darwin' ? true : false;
|
|
77
74
|
res['app.useNativeMenu'] = false;
|
|
78
75
|
}
|
|
76
|
+
for (const envVar in process.env) {
|
|
77
|
+
if (envVar.startsWith('SETTINGS_')) {
|
|
78
|
+
const key = envVar.substring('SETTINGS_'.length);
|
|
79
|
+
if (!res[key]) {
|
|
80
|
+
res[key] = process.env[envVar];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
79
84
|
return res;
|
|
80
85
|
},
|
|
81
86
|
|
|
87
|
+
async loadSettings() {
|
|
88
|
+
try {
|
|
89
|
+
const settingsText = await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' });
|
|
90
|
+
return this.fillMissingSettings(JSON.parse(settingsText));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return this.fillMissingSettings({});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
82
96
|
updateSettings_meta: true,
|
|
83
97
|
async updateSettings(values, req) {
|
|
84
98
|
if (!hasPermission(`settings/change`, req)) return false;
|
|
85
99
|
|
|
86
|
-
const res = await lock.acquire('
|
|
87
|
-
const currentValue = await this.
|
|
100
|
+
const res = await lock.acquire('settings', async () => {
|
|
101
|
+
const currentValue = await this.loadSettings();
|
|
88
102
|
try {
|
|
89
103
|
const updated = {
|
|
90
104
|
...currentValue,
|
|
@@ -13,6 +13,7 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
|
|
|
13
13
|
const processArgs = require('../utility/processArgs');
|
|
14
14
|
const { safeJsonParse } = require('dbgate-tools');
|
|
15
15
|
const platformInfo = require('../utility/platformInfo');
|
|
16
|
+
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
|
|
16
17
|
|
|
17
18
|
function getNamedArgs() {
|
|
18
19
|
const res = {};
|
|
@@ -165,12 +166,12 @@ module.exports = {
|
|
|
165
166
|
},
|
|
166
167
|
|
|
167
168
|
list_meta: true,
|
|
168
|
-
async list() {
|
|
169
|
+
async list(_params, req) {
|
|
169
170
|
if (portalConnections) {
|
|
170
171
|
if (platformInfo.allowShellConnection) return portalConnections;
|
|
171
|
-
return portalConnections.map(maskConnection);
|
|
172
|
+
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req));
|
|
172
173
|
}
|
|
173
|
-
return this.datastore.find();
|
|
174
|
+
return (await this.datastore.find()).filter(x => connectionHasPermission(x, req));
|
|
174
175
|
},
|
|
175
176
|
|
|
176
177
|
test_meta: true,
|
|
@@ -217,16 +218,18 @@ module.exports = {
|
|
|
217
218
|
},
|
|
218
219
|
|
|
219
220
|
update_meta: true,
|
|
220
|
-
async update({ _id, values }) {
|
|
221
|
+
async update({ _id, values }, req) {
|
|
221
222
|
if (portalConnections) return;
|
|
223
|
+
testConnectionPermission(_id, req);
|
|
222
224
|
const res = await this.datastore.patch(_id, values);
|
|
223
225
|
socket.emitChanged('connection-list-changed');
|
|
224
226
|
return res;
|
|
225
227
|
},
|
|
226
228
|
|
|
227
229
|
updateDatabase_meta: true,
|
|
228
|
-
async updateDatabase({ conid, database, values }) {
|
|
230
|
+
async updateDatabase({ conid, database, values }, req) {
|
|
229
231
|
if (portalConnections) return;
|
|
232
|
+
testConnectionPermission(conid, req);
|
|
230
233
|
const conn = await this.datastore.get(conid);
|
|
231
234
|
let databases = (conn && conn.databases) || [];
|
|
232
235
|
if (databases.find(x => x.name == database)) {
|
|
@@ -242,8 +245,9 @@ module.exports = {
|
|
|
242
245
|
},
|
|
243
246
|
|
|
244
247
|
delete_meta: true,
|
|
245
|
-
async delete(connection) {
|
|
248
|
+
async delete(connection, req) {
|
|
246
249
|
if (portalConnections) return;
|
|
250
|
+
testConnectionPermission(connection, req);
|
|
247
251
|
const res = await this.datastore.remove(connection._id);
|
|
248
252
|
socket.emitChanged('connection-list-changed');
|
|
249
253
|
return res;
|
|
@@ -260,7 +264,8 @@ module.exports = {
|
|
|
260
264
|
},
|
|
261
265
|
|
|
262
266
|
get_meta: true,
|
|
263
|
-
async get({ conid }) {
|
|
267
|
+
async get({ conid }, req) {
|
|
268
|
+
testConnectionPermission(conid, req);
|
|
264
269
|
return this.getCore({ conid, mask: true });
|
|
265
270
|
},
|
|
266
271
|
|
|
@@ -26,6 +26,7 @@ const generateDeploySql = require('../shell/generateDeploySql');
|
|
|
26
26
|
const { createTwoFilesPatch } = require('diff');
|
|
27
27
|
const diff2htmlPage = require('../utility/diff2htmlPage');
|
|
28
28
|
const processArgs = require('../utility/processArgs');
|
|
29
|
+
const { testConnectionPermission } = require('../utility/hasPermission');
|
|
29
30
|
|
|
30
31
|
module.exports = {
|
|
31
32
|
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
|
|
@@ -130,7 +131,8 @@ module.exports = {
|
|
|
130
131
|
},
|
|
131
132
|
|
|
132
133
|
queryData_meta: true,
|
|
133
|
-
async queryData({ conid, database, sql }) {
|
|
134
|
+
async queryData({ conid, database, sql }, req) {
|
|
135
|
+
testConnectionPermission(conid, req);
|
|
134
136
|
console.log(`Processing query, conid=${conid}, database=${database}, sql=${sql}`);
|
|
135
137
|
const opened = await this.ensureOpened(conid, database);
|
|
136
138
|
// if (opened && opened.status && opened.status.name == 'error') {
|
|
@@ -141,14 +143,16 @@ module.exports = {
|
|
|
141
143
|
},
|
|
142
144
|
|
|
143
145
|
sqlSelect_meta: true,
|
|
144
|
-
async sqlSelect({ conid, database, select }) {
|
|
146
|
+
async sqlSelect({ conid, database, select }, req) {
|
|
147
|
+
testConnectionPermission(conid, req);
|
|
145
148
|
const opened = await this.ensureOpened(conid, database);
|
|
146
149
|
const res = await this.sendRequest(opened, { msgtype: 'sqlSelect', select });
|
|
147
150
|
return res;
|
|
148
151
|
},
|
|
149
152
|
|
|
150
153
|
runScript_meta: true,
|
|
151
|
-
async runScript({ conid, database, sql }) {
|
|
154
|
+
async runScript({ conid, database, sql }, req) {
|
|
155
|
+
testConnectionPermission(conid, req);
|
|
152
156
|
console.log(`Processing script, conid=${conid}, database=${database}, sql=${sql}`);
|
|
153
157
|
const opened = await this.ensureOpened(conid, database);
|
|
154
158
|
const res = await this.sendRequest(opened, { msgtype: 'runScript', sql });
|
|
@@ -156,13 +160,15 @@ module.exports = {
|
|
|
156
160
|
},
|
|
157
161
|
|
|
158
162
|
collectionData_meta: true,
|
|
159
|
-
async collectionData({ conid, database, options }) {
|
|
163
|
+
async collectionData({ conid, database, options }, req) {
|
|
164
|
+
testConnectionPermission(conid, req);
|
|
160
165
|
const opened = await this.ensureOpened(conid, database);
|
|
161
166
|
const res = await this.sendRequest(opened, { msgtype: 'collectionData', options });
|
|
162
167
|
return res.result || null;
|
|
163
168
|
},
|
|
164
169
|
|
|
165
|
-
async loadDataCore(msgtype, { conid, database, ...args }) {
|
|
170
|
+
async loadDataCore(msgtype, { conid, database, ...args }, req) {
|
|
171
|
+
testConnectionPermission(conid, req);
|
|
166
172
|
const opened = await this.ensureOpened(conid, database);
|
|
167
173
|
const res = await this.sendRequest(opened, { msgtype, ...args });
|
|
168
174
|
if (res.errorMessage) {
|
|
@@ -176,32 +182,38 @@ module.exports = {
|
|
|
176
182
|
},
|
|
177
183
|
|
|
178
184
|
loadKeys_meta: true,
|
|
179
|
-
async loadKeys({ conid, database, root, filter }) {
|
|
185
|
+
async loadKeys({ conid, database, root, filter }, req) {
|
|
186
|
+
testConnectionPermission(conid, req);
|
|
180
187
|
return this.loadDataCore('loadKeys', { conid, database, root, filter });
|
|
181
188
|
},
|
|
182
189
|
|
|
183
190
|
exportKeys_meta: true,
|
|
184
|
-
async exportKeys({ conid, database, options }) {
|
|
191
|
+
async exportKeys({ conid, database, options }, req) {
|
|
192
|
+
testConnectionPermission(conid, req);
|
|
185
193
|
return this.loadDataCore('exportKeys', { conid, database, options });
|
|
186
194
|
},
|
|
187
195
|
|
|
188
196
|
loadKeyInfo_meta: true,
|
|
189
|
-
async loadKeyInfo({ conid, database, key }) {
|
|
197
|
+
async loadKeyInfo({ conid, database, key }, req) {
|
|
198
|
+
testConnectionPermission(conid, req);
|
|
190
199
|
return this.loadDataCore('loadKeyInfo', { conid, database, key });
|
|
191
200
|
},
|
|
192
201
|
|
|
193
202
|
loadKeyTableRange_meta: true,
|
|
194
|
-
async loadKeyTableRange({ conid, database, key, cursor, count }) {
|
|
203
|
+
async loadKeyTableRange({ conid, database, key, cursor, count }, req) {
|
|
204
|
+
testConnectionPermission(conid, req);
|
|
195
205
|
return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count });
|
|
196
206
|
},
|
|
197
207
|
|
|
198
208
|
loadFieldValues_meta: true,
|
|
199
|
-
async loadFieldValues({ conid, database, schemaName, pureName, field, search }) {
|
|
209
|
+
async loadFieldValues({ conid, database, schemaName, pureName, field, search }, req) {
|
|
210
|
+
testConnectionPermission(conid, req);
|
|
200
211
|
return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search });
|
|
201
212
|
},
|
|
202
213
|
|
|
203
214
|
callMethod_meta: true,
|
|
204
|
-
async callMethod({ conid, database, method, args }) {
|
|
215
|
+
async callMethod({ conid, database, method, args }, req) {
|
|
216
|
+
testConnectionPermission(conid, req);
|
|
205
217
|
return this.loadDataCore('callMethod', { conid, database, method, args });
|
|
206
218
|
|
|
207
219
|
// const opened = await this.ensureOpened(conid, database);
|
|
@@ -213,7 +225,8 @@ module.exports = {
|
|
|
213
225
|
},
|
|
214
226
|
|
|
215
227
|
updateCollection_meta: true,
|
|
216
|
-
async updateCollection({ conid, database, changeSet }) {
|
|
228
|
+
async updateCollection({ conid, database, changeSet }, req) {
|
|
229
|
+
testConnectionPermission(conid, req);
|
|
217
230
|
const opened = await this.ensureOpened(conid, database);
|
|
218
231
|
const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet });
|
|
219
232
|
if (res.errorMessage) {
|
|
@@ -225,7 +238,14 @@ module.exports = {
|
|
|
225
238
|
},
|
|
226
239
|
|
|
227
240
|
status_meta: true,
|
|
228
|
-
async status({ conid, database }) {
|
|
241
|
+
async status({ conid, database }, req) {
|
|
242
|
+
if (!conid) {
|
|
243
|
+
return {
|
|
244
|
+
name: 'error',
|
|
245
|
+
message: 'No connection',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
testConnectionPermission(conid, req);
|
|
229
249
|
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
|
230
250
|
if (existing) {
|
|
231
251
|
return {
|
|
@@ -247,7 +267,8 @@ module.exports = {
|
|
|
247
267
|
},
|
|
248
268
|
|
|
249
269
|
ping_meta: true,
|
|
250
|
-
async ping({ conid, database }) {
|
|
270
|
+
async ping({ conid, database }, req) {
|
|
271
|
+
testConnectionPermission(conid, req);
|
|
251
272
|
let existing = this.opened.find(x => x.conid == conid && x.database == database);
|
|
252
273
|
|
|
253
274
|
if (existing) {
|
|
@@ -263,7 +284,8 @@ module.exports = {
|
|
|
263
284
|
},
|
|
264
285
|
|
|
265
286
|
refresh_meta: true,
|
|
266
|
-
async refresh({ conid, database, keepOpen }) {
|
|
287
|
+
async refresh({ conid, database, keepOpen }, req) {
|
|
288
|
+
testConnectionPermission(conid, req);
|
|
267
289
|
if (!keepOpen) this.close(conid, database);
|
|
268
290
|
|
|
269
291
|
await this.ensureOpened(conid, database);
|
|
@@ -271,7 +293,8 @@ module.exports = {
|
|
|
271
293
|
},
|
|
272
294
|
|
|
273
295
|
syncModel_meta: true,
|
|
274
|
-
async syncModel({ conid, database, isFullRefresh }) {
|
|
296
|
+
async syncModel({ conid, database, isFullRefresh }, req) {
|
|
297
|
+
testConnectionPermission(conid, req);
|
|
275
298
|
const conn = await this.ensureOpened(conid, database);
|
|
276
299
|
conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh });
|
|
277
300
|
return { status: 'ok' };
|
|
@@ -301,13 +324,15 @@ module.exports = {
|
|
|
301
324
|
},
|
|
302
325
|
|
|
303
326
|
disconnect_meta: true,
|
|
304
|
-
async disconnect({ conid, database }) {
|
|
327
|
+
async disconnect({ conid, database }, req) {
|
|
328
|
+
testConnectionPermission(conid, req);
|
|
305
329
|
await this.close(conid, database, true);
|
|
306
330
|
return { status: 'ok' };
|
|
307
331
|
},
|
|
308
332
|
|
|
309
333
|
structure_meta: true,
|
|
310
|
-
async structure({ conid, database }) {
|
|
334
|
+
async structure({ conid, database }, req) {
|
|
335
|
+
testConnectionPermission(conid, req);
|
|
311
336
|
if (conid == '__model') {
|
|
312
337
|
const model = await importDbModel(database);
|
|
313
338
|
return model;
|
|
@@ -324,14 +349,19 @@ module.exports = {
|
|
|
324
349
|
},
|
|
325
350
|
|
|
326
351
|
serverVersion_meta: true,
|
|
327
|
-
async serverVersion({ conid, database }) {
|
|
352
|
+
async serverVersion({ conid, database }, req) {
|
|
353
|
+
if (!conid) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
testConnectionPermission(conid, req);
|
|
328
357
|
if (!conid) return null;
|
|
329
358
|
const opened = await this.ensureOpened(conid, database);
|
|
330
359
|
return opened.serverVersion || null;
|
|
331
360
|
},
|
|
332
361
|
|
|
333
362
|
sqlPreview_meta: true,
|
|
334
|
-
async sqlPreview({ conid, database, objects, options }) {
|
|
363
|
+
async sqlPreview({ conid, database, objects, options }, req) {
|
|
364
|
+
testConnectionPermission(conid, req);
|
|
335
365
|
// wait for structure
|
|
336
366
|
await this.structure({ conid, database });
|
|
337
367
|
|
|
@@ -341,7 +371,8 @@ module.exports = {
|
|
|
341
371
|
},
|
|
342
372
|
|
|
343
373
|
exportModel_meta: true,
|
|
344
|
-
async exportModel({ conid, database }) {
|
|
374
|
+
async exportModel({ conid, database }, req) {
|
|
375
|
+
testConnectionPermission(conid, req);
|
|
345
376
|
const archiveFolder = await archive.getNewArchiveFolder({ database });
|
|
346
377
|
await fs.mkdir(path.join(archivedir(), archiveFolder));
|
|
347
378
|
const model = await this.structure({ conid, database });
|
|
@@ -351,7 +382,8 @@ module.exports = {
|
|
|
351
382
|
},
|
|
352
383
|
|
|
353
384
|
generateDeploySql_meta: true,
|
|
354
|
-
async generateDeploySql({ conid, database, archiveFolder }) {
|
|
385
|
+
async generateDeploySql({ conid, database, archiveFolder }, req) {
|
|
386
|
+
testConnectionPermission(conid, req);
|
|
355
387
|
const opened = await this.ensureOpened(conid, database);
|
|
356
388
|
const res = await this.sendRequest(opened, {
|
|
357
389
|
msgtype: 'generateDeploySql',
|
|
@@ -7,6 +7,7 @@ const { handleProcessCommunication } = require('../utility/processComm');
|
|
|
7
7
|
const lock = new AsyncLock();
|
|
8
8
|
const config = require('./config');
|
|
9
9
|
const processArgs = require('../utility/processArgs');
|
|
10
|
+
const { testConnectionPermission } = require('../utility/hasPermission');
|
|
10
11
|
|
|
11
12
|
module.exports = {
|
|
12
13
|
opened: [],
|
|
@@ -90,19 +91,22 @@ module.exports = {
|
|
|
90
91
|
},
|
|
91
92
|
|
|
92
93
|
disconnect_meta: true,
|
|
93
|
-
async disconnect({ conid }) {
|
|
94
|
+
async disconnect({ conid }, req) {
|
|
95
|
+
testConnectionPermission(conid, req);
|
|
94
96
|
await this.close(conid, true);
|
|
95
97
|
return { status: 'ok' };
|
|
96
98
|
},
|
|
97
99
|
|
|
98
100
|
listDatabases_meta: true,
|
|
99
|
-
async listDatabases({ conid }) {
|
|
101
|
+
async listDatabases({ conid }, req) {
|
|
102
|
+
testConnectionPermission(conid, req);
|
|
100
103
|
const opened = await this.ensureOpened(conid);
|
|
101
104
|
return opened.databases;
|
|
102
105
|
},
|
|
103
106
|
|
|
104
107
|
version_meta: true,
|
|
105
|
-
async version({ conid }) {
|
|
108
|
+
async version({ conid }, req) {
|
|
109
|
+
testConnectionPermission(conid, req);
|
|
106
110
|
const opened = await this.ensureOpened(conid);
|
|
107
111
|
return opened.version;
|
|
108
112
|
},
|
|
@@ -132,7 +136,8 @@ module.exports = {
|
|
|
132
136
|
},
|
|
133
137
|
|
|
134
138
|
refresh_meta: true,
|
|
135
|
-
async refresh({ conid, keepOpen }) {
|
|
139
|
+
async refresh({ conid, keepOpen }, req) {
|
|
140
|
+
testConnectionPermission(conid, req);
|
|
136
141
|
if (!keepOpen) this.close(conid);
|
|
137
142
|
|
|
138
143
|
await this.ensureOpened(conid);
|
|
@@ -140,7 +145,8 @@ module.exports = {
|
|
|
140
145
|
},
|
|
141
146
|
|
|
142
147
|
createDatabase_meta: true,
|
|
143
|
-
async createDatabase({ conid, name }) {
|
|
148
|
+
async createDatabase({ conid, name }, req) {
|
|
149
|
+
testConnectionPermission(conid, req);
|
|
144
150
|
const opened = await this.ensureOpened(conid);
|
|
145
151
|
if (opened.connection.isReadOnly) return false;
|
|
146
152
|
opened.subprocess.send({ msgtype: 'createDatabase', name });
|
package/src/currentVersion.js
CHANGED
package/src/main.js
CHANGED
|
@@ -25,6 +25,7 @@ const plugins = require('./controllers/plugins');
|
|
|
25
25
|
const files = require('./controllers/files');
|
|
26
26
|
const scheduler = require('./controllers/scheduler');
|
|
27
27
|
const queryHistory = require('./controllers/queryHistory');
|
|
28
|
+
const onFinished = require('on-finished');
|
|
28
29
|
|
|
29
30
|
const { rundir } = require('./utility/directories');
|
|
30
31
|
const platformInfo = require('./utility/platformInfo');
|
|
@@ -63,7 +64,10 @@ function start() {
|
|
|
63
64
|
|
|
64
65
|
// Tell the client to retry every 10 seconds if connectivity is lost
|
|
65
66
|
res.write('retry: 10000\n\n');
|
|
66
|
-
socket.
|
|
67
|
+
socket.addSseResponse(res);
|
|
68
|
+
onFinished(req, () => {
|
|
69
|
+
socket.removeSseResponse(res);
|
|
70
|
+
});
|
|
67
71
|
});
|
|
68
72
|
|
|
69
73
|
app.use(bodyParser.json({ limit: '50mb' }));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const platformInfo = require('../utility/platformInfo');
|
|
3
3
|
const childProcessChecker = require('../utility/childProcessChecker');
|
|
4
|
-
const { SSHConnection } = require('node-ssh-forward');
|
|
5
4
|
const { handleProcessCommunication } = require('../utility/processComm');
|
|
5
|
+
const { SSHConnection } = require('../utility/SSHConnection');
|
|
6
6
|
|
|
7
7
|
async function getSshConnection(connection) {
|
|
8
8
|
const sshConfig = {
|
|
@@ -35,6 +35,8 @@ async function handleStart({ connection, tunnelConfig }) {
|
|
|
35
35
|
tunnelConfig,
|
|
36
36
|
});
|
|
37
37
|
} catch (err) {
|
|
38
|
+
console.log('Error creating SSH tunnel connection:', err.message);
|
|
39
|
+
|
|
38
40
|
process.send({
|
|
39
41
|
msgtype: 'error',
|
|
40
42
|
connection,
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2018 Stocard GmbH.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { Client } = require('ssh2');
|
|
18
|
+
const net = require('net');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const debug = require('debug');
|
|
23
|
+
|
|
24
|
+
// interface Options {
|
|
25
|
+
// username?: string;
|
|
26
|
+
// password?: string;
|
|
27
|
+
// privateKey?: string | Buffer;
|
|
28
|
+
// agentForward?: boolean;
|
|
29
|
+
// bastionHost?: string;
|
|
30
|
+
// passphrase?: string;
|
|
31
|
+
// endPort?: number;
|
|
32
|
+
// endHost: string;
|
|
33
|
+
// agentSocket?: string;
|
|
34
|
+
// skipAutoPrivateKey?: boolean;
|
|
35
|
+
// noReadline?: boolean;
|
|
36
|
+
// }
|
|
37
|
+
|
|
38
|
+
// interface ForwardingOptions {
|
|
39
|
+
// fromPort: number;
|
|
40
|
+
// toPort: number;
|
|
41
|
+
// toHost?: string;
|
|
42
|
+
// }
|
|
43
|
+
|
|
44
|
+
class SSHConnection {
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
this.debug = debug('ssh');
|
|
48
|
+
this.connections = [];
|
|
49
|
+
this.isWindows = process.platform === 'win32';
|
|
50
|
+
if (!options.username) {
|
|
51
|
+
this.options.username = process.env['SSH_USERNAME'] || process.env['USER'];
|
|
52
|
+
}
|
|
53
|
+
if (!options.endPort) {
|
|
54
|
+
this.options.endPort = 22;
|
|
55
|
+
}
|
|
56
|
+
if (!options.privateKey && !options.agentForward && !options.skipAutoPrivateKey) {
|
|
57
|
+
const defaultFilePath = path.join(os.homedir(), '.ssh', 'id_rsa');
|
|
58
|
+
if (fs.existsSync(defaultFilePath)) {
|
|
59
|
+
this.options.privateKey = fs.readFileSync(defaultFilePath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async shutdown() {
|
|
65
|
+
this.debug('Shutdown connections');
|
|
66
|
+
for (const connection of this.connections) {
|
|
67
|
+
connection.removeAllListeners();
|
|
68
|
+
connection.end();
|
|
69
|
+
}
|
|
70
|
+
return new Promise(resolve => {
|
|
71
|
+
if (this.server) {
|
|
72
|
+
this.server.close(resolve);
|
|
73
|
+
}
|
|
74
|
+
return resolve();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async tty() {
|
|
79
|
+
const connection = await this.establish();
|
|
80
|
+
this.debug('Opening tty');
|
|
81
|
+
await this.shell(connection);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async executeCommand(command) {
|
|
85
|
+
const connection = await this.establish();
|
|
86
|
+
this.debug('Executing command "%s"', command);
|
|
87
|
+
await this.shell(connection, command);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async shell(connection, command) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
connection.shell((err, stream) => {
|
|
93
|
+
if (err) {
|
|
94
|
+
return reject(err);
|
|
95
|
+
}
|
|
96
|
+
stream
|
|
97
|
+
.on('close', async () => {
|
|
98
|
+
stream.end();
|
|
99
|
+
process.stdin.unpipe(stream);
|
|
100
|
+
process.stdin.destroy();
|
|
101
|
+
connection.end();
|
|
102
|
+
await this.shutdown();
|
|
103
|
+
return resolve();
|
|
104
|
+
})
|
|
105
|
+
.stderr.on('data', data => {
|
|
106
|
+
return reject(data);
|
|
107
|
+
});
|
|
108
|
+
stream.pipe(process.stdout);
|
|
109
|
+
|
|
110
|
+
if (command) {
|
|
111
|
+
stream.end(`${command}\nexit\n`);
|
|
112
|
+
} else {
|
|
113
|
+
process.stdin.pipe(stream);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async establish() {
|
|
120
|
+
let connection;
|
|
121
|
+
if (this.options.bastionHost) {
|
|
122
|
+
connection = await this.connectViaBastion(this.options.bastionHost);
|
|
123
|
+
} else {
|
|
124
|
+
connection = await this.connect(this.options.endHost);
|
|
125
|
+
}
|
|
126
|
+
return connection;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async connectViaBastion(bastionHost) {
|
|
130
|
+
this.debug('Connecting to bastion host "%s"', bastionHost);
|
|
131
|
+
const connectionToBastion = await this.connect(bastionHost);
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
connectionToBastion.forwardOut(
|
|
134
|
+
'127.0.0.1',
|
|
135
|
+
22,
|
|
136
|
+
this.options.endHost,
|
|
137
|
+
this.options.endPort || 22,
|
|
138
|
+
async (err, stream) => {
|
|
139
|
+
if (err) {
|
|
140
|
+
return reject(err);
|
|
141
|
+
}
|
|
142
|
+
const connection = await this.connect(this.options.endHost, stream);
|
|
143
|
+
return resolve(connection);
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async connect(host, stream) {
|
|
150
|
+
this.debug('Connecting to "%s"', host);
|
|
151
|
+
const connection = new Client();
|
|
152
|
+
return new Promise(async (resolve, reject) => {
|
|
153
|
+
const options = {
|
|
154
|
+
host,
|
|
155
|
+
port: this.options.endPort,
|
|
156
|
+
username: this.options.username,
|
|
157
|
+
password: this.options.password,
|
|
158
|
+
privateKey: this.options.privateKey,
|
|
159
|
+
};
|
|
160
|
+
if (this.options.agentForward) {
|
|
161
|
+
options['agentForward'] = true;
|
|
162
|
+
|
|
163
|
+
// see https://github.com/mscdex/ssh2#client for agents on Windows
|
|
164
|
+
// guaranteed to give the ssh agent sock if the agent is running (posix)
|
|
165
|
+
let agentDefault = process.env['SSH_AUTH_SOCK'];
|
|
166
|
+
if (this.isWindows) {
|
|
167
|
+
// null or undefined
|
|
168
|
+
if (agentDefault == null) {
|
|
169
|
+
agentDefault = 'pageant';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const agentSock = this.options.agentSocket ? this.options.agentSocket : agentDefault;
|
|
174
|
+
if (agentSock == null) {
|
|
175
|
+
throw new Error('SSH Agent Socket is not provided, or is not set in the SSH_AUTH_SOCK env variable');
|
|
176
|
+
}
|
|
177
|
+
options['agent'] = agentSock;
|
|
178
|
+
}
|
|
179
|
+
if (stream) {
|
|
180
|
+
options['sock'] = stream;
|
|
181
|
+
}
|
|
182
|
+
// PPK private keys can be encrypted, but won't contain the word 'encrypted'
|
|
183
|
+
// in fact they always contain a `encryption` header, so we can't do a simple check
|
|
184
|
+
options['passphrase'] = this.options.passphrase;
|
|
185
|
+
const looksEncrypted = this.options.privateKey
|
|
186
|
+
? this.options.privateKey.toString().toLowerCase().includes('encrypted')
|
|
187
|
+
: false;
|
|
188
|
+
if (looksEncrypted && !options['passphrase'] && !this.options.noReadline) {
|
|
189
|
+
// options['passphrase'] = await this.getPassphrase();
|
|
190
|
+
}
|
|
191
|
+
connection.on('ready', () => {
|
|
192
|
+
this.connections.push(connection);
|
|
193
|
+
return resolve(connection);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
connection.on('error', error => {
|
|
197
|
+
reject(error);
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
connection.connect(options);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
reject(error);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// private async getPassphrase() {
|
|
208
|
+
// return new Promise(resolve => {
|
|
209
|
+
// const rl = readline.createInterface({
|
|
210
|
+
// input: process.stdin,
|
|
211
|
+
// output: process.stdout,
|
|
212
|
+
// });
|
|
213
|
+
// rl.question('Please type in the passphrase for your private key: ', answer => {
|
|
214
|
+
// return resolve(answer);
|
|
215
|
+
// });
|
|
216
|
+
// });
|
|
217
|
+
// }
|
|
218
|
+
|
|
219
|
+
async forward(options) {
|
|
220
|
+
const connection = await this.establish();
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
this.server = net
|
|
223
|
+
.createServer(socket => {
|
|
224
|
+
this.debug(
|
|
225
|
+
'Forwarding connection from "localhost:%d" to "%s:%d"',
|
|
226
|
+
options.fromPort,
|
|
227
|
+
options.toHost,
|
|
228
|
+
options.toPort
|
|
229
|
+
);
|
|
230
|
+
connection.forwardOut(
|
|
231
|
+
'localhost',
|
|
232
|
+
options.fromPort,
|
|
233
|
+
options.toHost || 'localhost',
|
|
234
|
+
options.toPort,
|
|
235
|
+
(error, stream) => {
|
|
236
|
+
if (error) {
|
|
237
|
+
return reject(error);
|
|
238
|
+
}
|
|
239
|
+
socket.pipe(stream);
|
|
240
|
+
stream.pipe(socket);
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
})
|
|
244
|
+
.listen(options.fromPort, 'localhost', () => {
|
|
245
|
+
return resolve();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = { SSHConnection };
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
const { SSHConnection } = require('node-ssh-forward');
|
|
2
|
-
const portfinder = require('portfinder');
|
|
3
1
|
const fs = require('fs-extra');
|
|
4
2
|
const { decryptConnection } = require('./crypting');
|
|
5
|
-
const { getSshTunnel } = require('./sshTunnel');
|
|
6
3
|
const { getSshTunnelProxy } = require('./sshTunnelProxy');
|
|
7
4
|
const platformInfo = require('../utility/platformInfo');
|
|
8
5
|
const connections = require('../controllers/connections');
|
|
@@ -4,12 +4,21 @@ const _ = require('lodash');
|
|
|
4
4
|
const userPermissions = {};
|
|
5
5
|
|
|
6
6
|
function hasPermission(tested, req) {
|
|
7
|
+
if (!req) {
|
|
8
|
+
// request object not available, allow all
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
7
11
|
const { user } = (req && req.auth) || {};
|
|
8
12
|
const key = user || '';
|
|
9
13
|
const logins = getLogins();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
if (!userPermissions[key]) {
|
|
16
|
+
if (logins) {
|
|
17
|
+
const login = logins.find(x => x.login == user);
|
|
18
|
+
userPermissions[key] = compilePermissions(login ? login.permissions : null);
|
|
19
|
+
} else {
|
|
20
|
+
userPermissions[key] = compilePermissions(process.env.PERMISSIONS);
|
|
21
|
+
}
|
|
13
22
|
}
|
|
14
23
|
return testPermission(tested, userPermissions[key]);
|
|
15
24
|
}
|
|
@@ -50,7 +59,26 @@ function getLogins() {
|
|
|
50
59
|
return loginsCache;
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
function connectionHasPermission(connection, req) {
|
|
63
|
+
if (!connection) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (_.isString(connection)) {
|
|
67
|
+
return hasPermission(`connections/${connection}`, req);
|
|
68
|
+
} else {
|
|
69
|
+
return hasPermission(`connections/${connection._id}`, req);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function testConnectionPermission(connection, req) {
|
|
74
|
+
if (!connectionHasPermission(connection, req)) {
|
|
75
|
+
throw new Error('Connection permission not granted');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
module.exports = {
|
|
54
80
|
hasPermission,
|
|
55
81
|
getLogins,
|
|
82
|
+
connectionHasPermission,
|
|
83
|
+
testConnectionPermission,
|
|
56
84
|
};
|
package/src/utility/socket.js
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
|
|
3
|
+
const sseResponses = [];
|
|
2
4
|
let electronSender = null;
|
|
3
|
-
let
|
|
5
|
+
let pingConfigured = false;
|
|
4
6
|
|
|
5
7
|
module.exports = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
ensurePing() {
|
|
9
|
+
if (!pingConfigured) {
|
|
10
|
+
setInterval(() => this.emit('ping'), 29 * 1000);
|
|
11
|
+
pingConfigured = true;
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
addSseResponse(value) {
|
|
15
|
+
sseResponses.push(value);
|
|
16
|
+
this.ensurePing();
|
|
17
|
+
},
|
|
18
|
+
removeSseResponse(value) {
|
|
19
|
+
_.remove(sseResponses, x => x == value);
|
|
9
20
|
},
|
|
10
21
|
setElectronSender(value) {
|
|
11
22
|
electronSender = value;
|
|
23
|
+
this.ensurePing();
|
|
12
24
|
},
|
|
13
25
|
emit(message, data) {
|
|
14
26
|
if (electronSender) {
|
|
15
|
-
if (init.length > 0) {
|
|
16
|
-
for (const item of init) {
|
|
17
|
-
electronSender.send(item.message, item.data == null ? null : item.data);
|
|
18
|
-
}
|
|
19
|
-
init = [];
|
|
20
|
-
}
|
|
21
27
|
electronSender.send(message, data == null ? null : data);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
sseResponse.write(
|
|
26
|
-
`event: ${item.message}\ndata: ${JSON.stringify(item.data == null ? null : item.data)}\n\n`
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
init = [];
|
|
30
|
-
}
|
|
31
|
-
sseResponse.write(`event: ${message}\ndata: ${JSON.stringify(data == null ? null : data)}\n\n`);
|
|
32
|
-
} else {
|
|
33
|
-
init.push([{ message, data }]);
|
|
28
|
+
}
|
|
29
|
+
for (const res of sseResponses) {
|
|
30
|
+
res.write(`event: ${message}\ndata: ${JSON.stringify(data == null ? null : data)}\n\n`);
|
|
34
31
|
}
|
|
35
32
|
},
|
|
36
33
|
emitChanged(key) {
|
|
@@ -47,7 +47,6 @@ module.exports = function useController(app, electron, route, controller) {
|
|
|
47
47
|
|
|
48
48
|
let method = 'post';
|
|
49
49
|
let raw = false;
|
|
50
|
-
let rawParams = false;
|
|
51
50
|
|
|
52
51
|
// if (_.isString(meta)) {
|
|
53
52
|
// method = meta;
|
|
@@ -55,7 +54,6 @@ module.exports = function useController(app, electron, route, controller) {
|
|
|
55
54
|
if (_.isPlainObject(meta)) {
|
|
56
55
|
method = meta.method;
|
|
57
56
|
raw = meta.raw;
|
|
58
|
-
rawParams = meta.rawParams;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
if (raw) {
|
|
@@ -67,9 +65,7 @@ module.exports = function useController(app, electron, route, controller) {
|
|
|
67
65
|
// controller._init_called = true;
|
|
68
66
|
// }
|
|
69
67
|
try {
|
|
70
|
-
|
|
71
|
-
if (rawParams) params = [req, res];
|
|
72
|
-
const data = await controller[key](...params);
|
|
68
|
+
const data = await controller[key]({ ...req.body, ...req.query }, req);
|
|
73
69
|
res.json(data);
|
|
74
70
|
} catch (e) {
|
|
75
71
|
console.log(e);
|