dbgate-api-premium 6.5.3 → 6.5.5

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.
@@ -0,0 +1,288 @@
1
+ // *** This file is part of DbGate Premium ***
2
+
3
+ const _ = require('lodash');
4
+ const { getLogger, extractErrorLogData } = require('dbgate-tools');
5
+ const { storageSqlCommandFmt, storageSelectFmt } = require('../controllers/storageDb');
6
+ const logger = getLogger('auditLog');
7
+
8
+ let auditLogQueue = [];
9
+ let isProcessing = false;
10
+ let isPlanned = false;
11
+
12
+ function nullableSum(a, b) {
13
+ const res = (a || 0) + (b || 0);
14
+ return res == 0 ? null : res;
15
+ }
16
+
17
+ async function processAuditLogQueue() {
18
+ do {
19
+ isProcessing = true;
20
+ const elements = [...auditLogQueue];
21
+ auditLogQueue = [];
22
+
23
+ while (elements.length > 0) {
24
+ const element = elements.shift();
25
+ if (!element) continue;
26
+ if (element.sessionId && element.sessionGroup && element.sessionParam) {
27
+ const existingRows = await storageSelectFmt(
28
+ '^select ~id, ~sumint_1, ~sumint_2 from ~audit_log where ~session_id = %v and ~session_group = %v and ~session_param = %v',
29
+ element.sessionId,
30
+ element.sessionGroup,
31
+ element.sessionParam
32
+ );
33
+ if (existingRows && existingRows.length > 0) {
34
+ const existing = existingRows[0];
35
+ await storageSqlCommandFmt(
36
+ '^update ~audit_log set ~sumint_1 = %v, ~sumint_2 = %v, ~modified = %v where ~id = %v',
37
+ nullableSum(element.sumint1, existing.sumint_1),
38
+ nullableSum(element.sumint2, existing.sumint_2),
39
+ element.created,
40
+ existing.id
41
+ );
42
+ // only update existing session
43
+ continue;
44
+ }
45
+ }
46
+ try {
47
+ let connectionData = null;
48
+ if (element.conid) {
49
+ const connections = await storageSelectFmt('^select * from ~connections where ~conid = %v', element.conid);
50
+ if (connections[0])
51
+ connectionData = _.pick(connections[0], [
52
+ 'displayName',
53
+ 'engine',
54
+ 'displayName',
55
+ 'databaseUrl',
56
+ 'singleDatabase',
57
+ 'server',
58
+ 'databaseFile',
59
+ 'useSshTunnel',
60
+ 'sshHost',
61
+ 'defaultDatabase',
62
+ ]);
63
+ }
64
+
65
+ const detailText = _.isPlainObject(element.detail) ? JSON.stringify(element.detail) : element.detail || null;
66
+ const connectionDataText = connectionData ? JSON.stringify(connectionData) : null;
67
+ await storageSqlCommandFmt(
68
+ `^insert ^into ~audit_log (
69
+ ~user_id, ~user_login, ~created, ~category, ~component, ~event, ~detail, ~detail_full_length, ~action, ~severity,
70
+ ~conid, ~database, ~schema_name, ~pure_name, ~sumint_1, ~sumint_2, ~session_id, ~session_group, ~session_param, ~connection_data, ~message)
71
+ values (%v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v)`,
72
+ element.userId || null,
73
+ element.login || null,
74
+ element.created,
75
+ element.category || null,
76
+ element.component || null,
77
+ element.event || null,
78
+ detailText?.slice(0, 1000) || null,
79
+ detailText?.length || null,
80
+ element.action || null,
81
+ element.severity || 'info',
82
+ element.conid || null,
83
+ element.database || null,
84
+ element.schemaName || null,
85
+ element.pureName || null,
86
+ element.sumint1 || null,
87
+ element.sumint2 || null,
88
+ element.sessionId || null,
89
+ element.sessionGroup || null,
90
+ element.sessionParam || null,
91
+ connectionDataText?.slice(0, 1000) || null,
92
+ element.message || null
93
+ );
94
+ } catch (err) {
95
+ logger.error(extractErrorLogData(err), 'Error processing audit log entry');
96
+ }
97
+ }
98
+
99
+ isProcessing = false;
100
+ } while (auditLogQueue.length > 0);
101
+ isPlanned = false;
102
+ }
103
+
104
+ async function sendToAuditLog(
105
+ req,
106
+ {
107
+ category,
108
+ component,
109
+ event,
110
+ detail = null,
111
+ action,
112
+ severity = 'info',
113
+ conid = null,
114
+ database = null,
115
+ schemaName = null,
116
+ pureName = null,
117
+ sumint1 = null,
118
+ sumint2 = null,
119
+ sessionGroup = null,
120
+ sessionParam = null,
121
+ message = null,
122
+ }
123
+ ) {
124
+ if (!process.env.STORAGE_DATABASE) {
125
+ return;
126
+ }
127
+ const config = require('../controllers/config');
128
+ const settings = await config.getCachedSettings();
129
+ if (settings?.['storage.useAuditLog'] != 'true') {
130
+ return;
131
+ }
132
+
133
+ const { login, userId } = req?.user || {};
134
+ const sessionId = req?.headers?.['x-api-session-id'];
135
+
136
+ auditLogQueue.push({
137
+ userId,
138
+ login,
139
+ created: new Date().getTime(),
140
+ category,
141
+ component,
142
+ event,
143
+ detail,
144
+ action,
145
+ severity,
146
+ conid,
147
+ database,
148
+ schemaName,
149
+ pureName,
150
+ sumint1,
151
+ sumint2,
152
+ sessionId,
153
+ sessionGroup,
154
+ sessionParam,
155
+ message,
156
+ });
157
+ if (!isProcessing && !isPlanned) {
158
+ setTimeout(() => {
159
+ isPlanned = true;
160
+ processAuditLogQueue();
161
+ }, 0);
162
+ }
163
+ }
164
+
165
+ function maskLogFields(script) {
166
+ return _.cloneDeepWith(script, (value, key) => {
167
+ if (_.isString(key) && key.toLowerCase().includes('password')) {
168
+ return '****';
169
+ }
170
+ if (key == 'query') {
171
+ return '****';
172
+ }
173
+ });
174
+ }
175
+
176
+ function extractConnectionId(connection) {
177
+ if (
178
+ connection?.server == process.env.STORAGE_SERVER &&
179
+ connection?.database == process.env.STORAGE_DATABASE &&
180
+ connection?.port == process.env.STORAGE_PORT
181
+ ) {
182
+ return '__storage';
183
+ }
184
+ if (connection?.conid) {
185
+ return connection.conid;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ function analyseJsonRunnerScript(script) {
191
+ const [assignSource, assignTarget, copyStream] = _.isArray(script?.commands) ? script?.commands : [];
192
+ if (assignSource?.type != 'assign') {
193
+ return null;
194
+ }
195
+ if (assignTarget?.type != 'assign') {
196
+ return null;
197
+ }
198
+ if (copyStream?.type != 'copyStream') {
199
+ return null;
200
+ }
201
+
202
+ if (assignTarget?.functionName == 'tableWriter') {
203
+ const pureName = assignTarget?.props?.pureName;
204
+ const schemaName = assignTarget?.props?.schemaName;
205
+ const connection = assignTarget?.props?.connection;
206
+ if (pureName && connection) {
207
+ return {
208
+ category: 'import',
209
+ component: 'RunnersController',
210
+ event: 'import.table',
211
+ action: 'import',
212
+ severity: 'info',
213
+ message: `Importing table ${pureName}`,
214
+ pureName: pureName,
215
+ conid: extractConnectionId(connection),
216
+ database: connection?.database,
217
+ schemaName: schemaName,
218
+ detail: maskLogFields(script),
219
+ };
220
+ }
221
+ return null;
222
+ }
223
+
224
+ if (assignSource?.functionName == 'tableReader') {
225
+ const pureName = assignSource?.props?.pureName;
226
+ const schemaName = assignSource?.props?.schemaName;
227
+ const connection = assignSource?.props?.connection;
228
+ if (pureName && connection) {
229
+ return {
230
+ category: 'export',
231
+ component: 'RunnersController',
232
+ event: 'export.table',
233
+ action: 'export',
234
+ severity: 'info',
235
+ message: `Exporting table ${pureName}`,
236
+ pureName: pureName,
237
+ conid: extractConnectionId(connection),
238
+ database: connection?.database,
239
+ schemaName: schemaName,
240
+ detail: maskLogFields(script),
241
+ };
242
+ }
243
+ return null;
244
+ }
245
+
246
+ if (assignSource?.functionName == 'queryReader') {
247
+ const connection = assignSource?.props?.connection;
248
+ if (connection) {
249
+ return {
250
+ category: 'export',
251
+ component: 'RunnersController',
252
+ event: 'export.query',
253
+ action: 'export',
254
+ severity: 'info',
255
+ message: 'Exporting query',
256
+ conid: extractConnectionId(connection),
257
+ database: connection?.database,
258
+ detail: maskLogFields(script),
259
+ };
260
+ }
261
+ return null;
262
+ }
263
+
264
+ return null;
265
+ }
266
+
267
+ function logJsonRunnerScript(req, script) {
268
+ const analysed = analyseJsonRunnerScript(script);
269
+
270
+ if (analysed) {
271
+ sendToAuditLog(req, analysed);
272
+ } else {
273
+ sendToAuditLog(req, {
274
+ category: 'shell',
275
+ component: 'RunnersController',
276
+ event: 'script.run.json',
277
+ action: 'script',
278
+ severity: 'info',
279
+ detail: maskLogFields(script),
280
+ message: 'Running JSON script',
281
+ });
282
+ }
283
+ }
284
+
285
+ module.exports = {
286
+ sendToAuditLog,
287
+ logJsonRunnerScript,
288
+ };
@@ -31,12 +31,16 @@ function isAuthProxySupported() {
31
31
  return true;
32
32
  }
33
33
 
34
+ function getE2ETestHeaders() {
35
+ return processArgs.runE2eTests ? { 'x-api-key': 'bcf6e1a0-5763-4060-9391-18fda005722d' } : {};
36
+ }
37
+
34
38
  function getAxiosParamsWithLicense() {
35
39
  return {
36
40
  headers: {
37
41
  'Content-Type': 'application/json',
38
42
  Authorization: `Bearer ${licenseKey ?? process.env.DBGATE_LICENSE}`,
39
- 'x-api-key': processArgs.runE2eTests ? 'bcf6e1a0-5763-4060-9391-18fda005722d' : null,
43
+ ...getE2ETestHeaders(),
40
44
  },
41
45
  };
42
46
  }
@@ -44,10 +48,13 @@ function getAxiosParamsWithLicense() {
44
48
  function getLicenseHttpHeaders() {
45
49
  const licenseValue = licenseKey ?? process.env.DBGATE_LICENSE;
46
50
  if (!licenseValue) {
47
- return {};
51
+ return {
52
+ ...getE2ETestHeaders(),
53
+ };
48
54
  }
49
55
  return {
50
56
  'x-license': licenseValue,
57
+ ...getE2ETestHeaders(),
51
58
  };
52
59
  }
53
60
 
@@ -147,6 +147,8 @@ function checkLicenseKey(licenseKey) {
147
147
  expiration: new Date(decoded.exp * 1000).toISOString(),
148
148
  daysLeft: Math.round((decoded.exp * 1000 - Date.now()) / (24 * 60 * 60 * 1000)),
149
149
  licenseTypeObj,
150
+ licenseId: decoded.licenseId,
151
+ users: decoded.users,
150
152
  };
151
153
  } catch (err) {
152
154
  try {
@@ -34,11 +34,12 @@ const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD
34
34
  ? 'https://cloud.dbgate.udolni.net'
35
35
  : 'https://cloud.dbgate.io';
36
36
 
37
- async function createDbGateIdentitySession(client) {
37
+ async function createDbGateIdentitySession(client, redirectUri) {
38
38
  const resp = await axios.default.post(
39
39
  `${DBGATE_IDENTITY_URL}/api/create-session`,
40
40
  {
41
41
  client,
42
+ redirectUri,
42
43
  },
43
44
  {
44
45
  headers: {
@@ -70,7 +71,7 @@ function startCloudTokenChecking(sid, callback) {
70
71
  });
71
72
  // console.log('CHECK RESP:', resp.data);
72
73
 
73
- if (resp.data.email) {
74
+ if (resp.data?.email) {
74
75
  clearInterval(interval);
75
76
  callback(resp.data);
76
77
  }
@@ -80,6 +81,34 @@ function startCloudTokenChecking(sid, callback) {
80
81
  }, 500);
81
82
  }
82
83
 
84
+ async function readCloudTokenHolder(sid) {
85
+ const resp = await axios.default.get(`${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, {
86
+ headers: {
87
+ ...getLicenseHttpHeaders(),
88
+ },
89
+ });
90
+ if (resp.data?.email) {
91
+ return resp.data;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ async function readCloudTestTokenHolder(email) {
97
+ const resp = await axios.default.post(
98
+ `${DBGATE_IDENTITY_URL}/api/test-token`,
99
+ { email },
100
+ {
101
+ headers: {
102
+ ...getLicenseHttpHeaders(),
103
+ },
104
+ }
105
+ );
106
+ if (resp.data?.email) {
107
+ return resp.data;
108
+ }
109
+ return null;
110
+ }
111
+
83
112
  async function loadCloudFiles() {
84
113
  try {
85
114
  const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files.jsonl'), 'utf-8');
@@ -396,4 +425,6 @@ module.exports = {
396
425
  loadCachedCloudConnection,
397
426
  putCloudContent,
398
427
  removeCloudCachedConnection,
428
+ readCloudTokenHolder,
429
+ readCloudTestTokenHolder,
399
430
  };
@@ -4,7 +4,6 @@ const { getSshTunnelProxy } = require('./sshTunnelProxy');
4
4
  const platformInfo = require('../utility/platformInfo');
5
5
  const connections = require('../controllers/connections');
6
6
  const _ = require('lodash');
7
- const { getCloudFolderEncryptor } = require('./cloudIntf');
8
7
 
9
8
  async function loadConnection(driver, storedConnection, connectionMode) {
10
9
  const { allowShellConnection, allowConnectionFromEnvVariables } = platformInfo;
@@ -90,6 +89,8 @@ async function extractConnectionSslParams(connection) {
90
89
  }
91
90
 
92
91
  async function decryptCloudConnection(connection) {
92
+ const { getCloudFolderEncryptor } = require('./cloudIntf');
93
+
93
94
  const m = connection?._id?.match(/^cloud\:\/\/(.+)\/(.+)$/);
94
95
  if (!m) {
95
96
  throw new Error('Invalid cloud connection ID format');
@@ -1,4 +1,12 @@
1
- const getChartExport = (title, config, imageFile) => {
1
+ const getChartExport = (title, config, imageFile, plugins) => {
2
+ const PLUGIN_TAGS = {
3
+ zoom: '<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.2.0/chartjs-plugin-zoom.min.js" integrity="sha512-TT0wAMqqtjXVzpc48sI0G84rBP+oTkBZPgeRYIOVRGUdwJsyS3WPipsNh///ay2LJ+onCM23tipnz6EvEy2/UA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>',
4
+ dataLabels:
5
+ '<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>',
6
+ outlabels:
7
+ '<script src="https://cdn.jsdelivr.net/npm/@energiency/chartjs-plugin-piechart-outlabels@1.3.4/dist/chartjs-plugin-piechart-outlabels.min.js"></script>',
8
+ };
9
+
2
10
  return `<html>
3
11
  <meta charset='utf-8'>
4
12
 
@@ -8,7 +16,7 @@ const getChartExport = (title, config, imageFile) => {
8
16
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js" integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
9
17
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.0/chartjs-adapter-moment.min.js" integrity="sha512-oh5t+CdSBsaVVAvxcZKy3XJdP7ZbYUBSRCXDTVn0ODewMDDNnELsrG9eDm8rVZAQg7RsDD/8K3MjPAFB13o6eA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
10
18
  <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js" integrity="sha512-UXumZrZNiOwnTcZSHLOfcTs0aos2MzBWHXOHOuB0J/R44QB0dwY5JgfbvljXcklVf65Gc4El6RjZ+lnwd2az2g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
11
- <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.2.0/chartjs-plugin-zoom.min.js" integrity="sha512-TT0wAMqqtjXVzpc48sI0G84rBP+oTkBZPgeRYIOVRGUdwJsyS3WPipsNh///ay2LJ+onCM23tipnz6EvEy2/UA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
19
+ ${plugins.map(plugin => PLUGIN_TAGS[plugin] ?? '')}
12
20
 
13
21
  <style>
14
22
  a { text-decoration: none }
@@ -45,7 +53,7 @@ const getChartExport = (title, config, imageFile) => {
45
53
  </div>
46
54
 
47
55
  <div class="footer">
48
- Exported from <a href='https://dbgate.org/' target='_blank'>DbGate</a>, powered by <a href='https://www.chartjs.org/' target='_blank'>Chart.js</a>
56
+ Exported from <a href='https://dbgate.io/' target='_blank'>DbGate</a>, powered by <a href='https://www.chartjs.org/' target='_blank'>Chart.js</a>
49
57
  </div>
50
58
  </body>
51
59
 
@@ -18,7 +18,7 @@ const getMapExport = (geoJson) => {
18
18
  leaflet
19
19
  .tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
20
20
  maxZoom: 19,
21
- attribution: '<a href="https://dbgate.org" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
21
+ attribution: '<a href="https://dbgate.io" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
22
22
  })
23
23
  .addTo(map);
24
24
 
@@ -0,0 +1,44 @@
1
+ // *** This file is part of DbGate Premium ***
2
+
3
+ const _ = require('lodash');
4
+ const { sendToAuditLog } = require('./auditlog');
5
+ const { checkLicense } = require('./checkLicense');
6
+ const LOGIN_LIMIT_ERROR = 'Your limit of concurrent logins has been reached';
7
+
8
+ // map string (user key) => time to expiration
9
+ let activeLoggedUsers = {};
10
+
11
+ function markUserAsActive(licenseUid) {
12
+ activeLoggedUsers[licenseUid] = Date.now() + 60 * 1000; // mark user as active for 1 minute
13
+ }
14
+
15
+ async function isLoginLicensed(req, licenseUid) {
16
+ const license = await checkLicense();
17
+ activeLoggedUsers = _.pickBy(activeLoggedUsers, value => value > Date.now());
18
+ if (licenseUid in activeLoggedUsers) {
19
+ return true;
20
+ }
21
+ if (license.licenseId == 'f0346efe-ebc2-4822-9a83-4b4668a897e5' && license?.users != null) {
22
+ const currentUserCount = Object.keys(activeLoggedUsers).length;
23
+ if (currentUserCount >= license?.users) {
24
+ sendToAuditLog(req, {
25
+ category: 'auth',
26
+ component: 'StorageAuthProvider',
27
+ action: 'loginFail',
28
+ event: 'login.licenseFailed',
29
+ severity: 'warn',
30
+ detail: { licenseUid, activeUsers: currentUserCount, licenseUsers: license?.users },
31
+ message: `Login failed, too many active users: ${currentUserCount + 1}, license allows only ${license?.users}`,
32
+ });
33
+
34
+ return false;
35
+ }
36
+ }
37
+ return true;
38
+ }
39
+
40
+ module.exports = {
41
+ markUserAsActive,
42
+ isLoginLicensed,
43
+ LOGIN_LIMIT_ERROR,
44
+ };