dbgate-api 5.3.3 → 5.4.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api",
3
3
  "main": "src/index.js",
4
- "version": "5.3.3",
4
+ "version": "5.4.0",
5
5
  "homepage": "https://dbgate.org/",
6
6
  "repository": {
7
7
  "type": "git",
@@ -26,10 +26,10 @@
26
26
  "compare-versions": "^3.6.0",
27
27
  "cors": "^2.8.5",
28
28
  "cross-env": "^6.0.3",
29
- "dbgate-datalib": "^5.3.3",
30
- "dbgate-query-splitter": "^4.10.1",
31
- "dbgate-sqltree": "^5.3.3",
32
- "dbgate-tools": "^5.3.3",
29
+ "dbgate-datalib": "^5.4.0",
30
+ "dbgate-query-splitter": "^4.10.3",
31
+ "dbgate-sqltree": "^5.4.0",
32
+ "dbgate-tools": "^5.4.0",
33
33
  "debug": "^4.3.4",
34
34
  "diff": "^5.0.0",
35
35
  "diff2html": "^3.4.13",
@@ -66,6 +66,7 @@
66
66
  "start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api",
67
67
  "start:dblogin": "env-cmd -f env/dblogin/.env node src/index.js --listen-api",
68
68
  "start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
69
+ "start:storage": "env-cmd -f env/storage/.env node src/index.js --listen-api",
69
70
  "start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
70
71
  "ts": "tsc",
71
72
  "build": "webpack"
@@ -73,7 +74,7 @@
73
74
  "devDependencies": {
74
75
  "@types/fs-extra": "^9.0.11",
75
76
  "@types/lodash": "^4.14.149",
76
- "dbgate-types": "^5.3.3",
77
+ "dbgate-types": "^5.4.0",
77
78
  "env-cmd": "^10.1.0",
78
79
  "node-loader": "^1.0.2",
79
80
  "nodemon": "^2.0.2",
@@ -83,6 +84,7 @@
83
84
  },
84
85
  "optionalDependencies": {
85
86
  "better-sqlite3": "9.6.0",
86
- "msnodesqlv8": "^4.2.1"
87
+ "msnodesqlv8": "^4.2.1",
88
+ "oracledb": "^6.6.0"
87
89
  }
88
90
  }
@@ -0,0 +1,16 @@
1
+ const crypto = require('crypto');
2
+
3
+ const tokenSecret = crypto.randomUUID();
4
+
5
+ function getTokenLifetime() {
6
+ return process.env.TOKEN_LIFETIME || '1d';
7
+ }
8
+
9
+ function getTokenSecret() {
10
+ return tokenSecret;
11
+ }
12
+
13
+ module.exports = {
14
+ getTokenLifetime,
15
+ getTokenSecret,
16
+ };
@@ -0,0 +1,322 @@
1
+ const { getTokenSecret, getTokenLifetime } = require('./authCommon');
2
+ const _ = require('lodash');
3
+ const axios = require('axios');
4
+ const { getLogger, getPredefinedPermissions } = require('dbgate-tools');
5
+
6
+ const AD = require('activedirectory2').promiseWrapper;
7
+ const jwt = require('jsonwebtoken');
8
+
9
+ const logger = getLogger('authProvider');
10
+
11
+ class AuthProviderBase {
12
+ amoid = 'none';
13
+
14
+ async login(login, password, options = undefined) {
15
+ return {
16
+ accessToken: jwt.sign(
17
+ {
18
+ amoid: this.amoid,
19
+ },
20
+ getTokenSecret(),
21
+ { expiresIn: getTokenLifetime() }
22
+ ),
23
+ };
24
+ }
25
+
26
+ oauthToken(params) {
27
+ return {};
28
+ }
29
+
30
+ getCurrentLogin(req) {
31
+ const login = req?.user?.login ?? req?.auth?.user ?? null;
32
+ return login;
33
+ }
34
+
35
+ isUserLoggedIn(req) {
36
+ return !!req?.user || !!req?.auth;
37
+ }
38
+
39
+ getCurrentPermissions(req) {
40
+ const login = this.getCurrentLogin(req);
41
+ const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
42
+ return permissions || process.env.PERMISSIONS;
43
+ }
44
+
45
+ getLoginPageConnections() {
46
+ return null;
47
+ }
48
+
49
+ getSingleConnectionId(req) {
50
+ return null;
51
+ }
52
+
53
+ toJson() {
54
+ return {
55
+ amoid: this.amoid,
56
+ workflowType: 'anonymous',
57
+ name: 'Anonymous',
58
+ };
59
+ }
60
+
61
+ async redirect({ state }) {
62
+ return {
63
+ status: 'error',
64
+ };
65
+ }
66
+
67
+ async getLogoutUrl() {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ class OAuthProvider extends AuthProviderBase {
73
+ amoid = 'oauth';
74
+
75
+ async oauthToken(params) {
76
+ const { redirectUri, code } = params;
77
+
78
+ const scopeParam = process.env.OAUTH_SCOPE ? `&scope=${process.env.OAUTH_SCOPE}` : '';
79
+ const resp = await axios.default.post(
80
+ `${process.env.OAUTH_TOKEN}`,
81
+ `grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(
82
+ redirectUri
83
+ )}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}${scopeParam}`
84
+ );
85
+
86
+ const { access_token, refresh_token } = resp.data;
87
+
88
+ const payload = jwt.decode(access_token);
89
+
90
+ logger.info({ payload }, 'User payload returned from OAUTH');
91
+
92
+ const login =
93
+ process.env.OAUTH_LOGIN_FIELD && payload && payload[process.env.OAUTH_LOGIN_FIELD]
94
+ ? payload[process.env.OAUTH_LOGIN_FIELD]
95
+ : 'oauth';
96
+
97
+ if (
98
+ process.env.OAUTH_ALLOWED_LOGINS &&
99
+ !process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
100
+ ) {
101
+ return { error: `Username ${login} not allowed to log in` };
102
+ }
103
+
104
+ const groups =
105
+ process.env.OAUTH_GROUP_FIELD && payload && payload[process.env.OAUTH_GROUP_FIELD]
106
+ ? payload[process.env.OAUTH_GROUP_FIELD]
107
+ : [];
108
+
109
+ const allowedGroups = process.env.OAUTH_ALLOWED_GROUPS
110
+ ? process.env.OAUTH_ALLOWED_GROUPS.split(',').map(group => group.toLowerCase().trim())
111
+ : [];
112
+
113
+ if (process.env.OAUTH_ALLOWED_GROUPS && !groups.some(group => allowedGroups.includes(group.toLowerCase().trim()))) {
114
+ return { error: `Username ${login} does not belong to an allowed group` };
115
+ }
116
+
117
+ if (access_token) {
118
+ return {
119
+ accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }),
120
+ };
121
+ }
122
+
123
+ return { error: 'Token not found' };
124
+ }
125
+
126
+ async getLogoutUrl() {
127
+ return process.env.OAUTH_LOGOUT;
128
+ }
129
+
130
+ toJson() {
131
+ return {
132
+ ...super.toJson(),
133
+ workflowType: 'redirect',
134
+ name: 'OAuth 2.0',
135
+ };
136
+ }
137
+
138
+ redirect({ state, redirectUri }) {
139
+ const scopeParam = process.env.OAUTH_SCOPE ? `&scope=${process.env.OAUTH_SCOPE}` : '';
140
+ return {
141
+ status: 'ok',
142
+ uri: `${process.env.OAUTH_AUTH}?client_id=${
143
+ process.env.OAUTH_CLIENT_ID
144
+ }&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(
145
+ state
146
+ )}${scopeParam}`,
147
+ };
148
+ }
149
+ }
150
+
151
+ class ADProvider extends AuthProviderBase {
152
+ amoid = 'ad';
153
+
154
+ async login(login, password, options = undefined) {
155
+ const adConfig = {
156
+ url: process.env.AD_URL,
157
+ baseDN: process.env.AD_BASEDN,
158
+ username: process.env.AD_USERNAME,
159
+ password: process.env.AD_PASSWORD,
160
+ };
161
+ const ad = new AD(adConfig);
162
+ try {
163
+ const res = await ad.authenticate(login, password);
164
+ if (!res) {
165
+ return { error: 'Login failed' };
166
+ }
167
+ if (
168
+ process.env.AD_ALLOWED_LOGINS &&
169
+ !process.env.AD_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
170
+ ) {
171
+ return { error: `Username ${login} not allowed to log in` };
172
+ }
173
+ return {
174
+ accessToken: jwt.sign(
175
+ {
176
+ amoid: this.amoid,
177
+ login,
178
+ },
179
+ getTokenSecret(),
180
+ { expiresIn: getTokenLifetime() }
181
+ ),
182
+ };
183
+ } catch (e) {
184
+ return { error: 'Login failed' };
185
+ }
186
+ }
187
+
188
+ toJson() {
189
+ return {
190
+ ...super.toJson(),
191
+ workflowType: 'credentials',
192
+ name: 'Active Directory',
193
+ };
194
+ }
195
+ }
196
+
197
+ class LoginsProvider extends AuthProviderBase {
198
+ amoid = 'logins';
199
+
200
+ async login(login, password, options = undefined) {
201
+ if (password == process.env[`LOGIN_PASSWORD_${login}`]) {
202
+ return {
203
+ accessToken: jwt.sign(
204
+ {
205
+ amoid: this.amoid,
206
+ login,
207
+ },
208
+ getTokenSecret(),
209
+ { expiresIn: getTokenLifetime() }
210
+ ),
211
+ };
212
+ }
213
+ return { error: 'Invalid credentials' };
214
+ }
215
+
216
+ toJson() {
217
+ return {
218
+ ...super.toJson(),
219
+ workflowType: 'credentials',
220
+ name: 'Login & Password',
221
+ };
222
+ }
223
+ }
224
+
225
+ class DenyAllProvider extends AuthProviderBase {
226
+ amoid = 'deny';
227
+
228
+ async login(login, password, options = undefined) {
229
+ return { error: 'Login not allowed' };
230
+ }
231
+
232
+ toJson() {
233
+ return {
234
+ ...super.toJson(),
235
+ workflowType: 'credentials',
236
+ name: 'Deny all',
237
+ };
238
+ }
239
+ }
240
+
241
+ function hasEnvLogins() {
242
+ if (process.env.LOGIN && process.env.PASSWORD) {
243
+ return true;
244
+ }
245
+ for (const key in process.env) {
246
+ if (key.startsWith('LOGIN_PASSWORD_')) {
247
+ return true;
248
+ }
249
+ }
250
+ return false;
251
+ }
252
+
253
+ function detectEnvAuthProvider() {
254
+ if (process.env.AUTH_PROVIDER) {
255
+ return process.env.AUTH_PROVIDER;
256
+ }
257
+
258
+ if (process.env.STORAGE_DATABASE) {
259
+ return 'denyall';
260
+ }
261
+ if (process.env.OAUTH_AUTH) {
262
+ return 'oauth';
263
+ }
264
+ if (process.env.AD_URL) {
265
+ return 'ad';
266
+ }
267
+ if (hasEnvLogins()) {
268
+ return 'logins';
269
+ }
270
+ return 'none';
271
+ }
272
+
273
+ function createEnvAuthProvider() {
274
+ const authProvider = detectEnvAuthProvider();
275
+ switch (authProvider) {
276
+ case 'oauth':
277
+ return new OAuthProvider();
278
+ case 'ad':
279
+ return new ADProvider();
280
+ case 'logins':
281
+ return new LoginsProvider();
282
+ case 'denyall':
283
+ return new DenyAllProvider();
284
+ default:
285
+ return new AuthProviderBase();
286
+ }
287
+ }
288
+
289
+ let defaultAuthProvider = createEnvAuthProvider();
290
+ let authProviders = [defaultAuthProvider];
291
+
292
+ function getAuthProviders() {
293
+ return authProviders;
294
+ }
295
+
296
+ function getAuthProviderById(amoid) {
297
+ return authProviders.find(x => x.amoid == amoid);
298
+ }
299
+
300
+ function getDefaultAuthProvider() {
301
+ return defaultAuthProvider;
302
+ }
303
+
304
+ function getAuthProviderFromReq(req) {
305
+ const authProviderId = req?.auth?.amoid || req?.user?.amoid;
306
+ return getAuthProviderById(authProviderId) ?? getDefaultAuthProvider();
307
+ }
308
+
309
+ function setAuthProviders(value, defaultProvider = null) {
310
+ authProviders = value;
311
+ defaultAuthProvider = defaultProvider || value[0];
312
+ }
313
+
314
+ module.exports = {
315
+ AuthProviderBase,
316
+ detectEnvAuthProvider,
317
+ getAuthProviders,
318
+ getDefaultAuthProvider,
319
+ setAuthProviders,
320
+ getAuthProviderById,
321
+ getAuthProviderFromReq,
322
+ };
@@ -1,24 +1,20 @@
1
1
  const axios = require('axios');
2
2
  const jwt = require('jsonwebtoken');
3
3
  const getExpressPath = require('../utility/getExpressPath');
4
- const { getLogins } = require('../utility/hasPermission');
5
4
  const { getLogger } = require('dbgate-tools');
6
5
  const AD = require('activedirectory2').promiseWrapper;
7
6
  const crypto = require('crypto');
7
+ const { getTokenSecret, getTokenLifetime } = require('../auth/authCommon');
8
+ const {
9
+ getAuthProviderFromReq,
10
+ getAuthProviders,
11
+ getDefaultAuthProvider,
12
+ getAuthProviderById,
13
+ } = require('../auth/authProvider');
14
+ const storage = require('./storage');
8
15
 
9
16
  const logger = getLogger('auth');
10
17
 
11
- const tokenSecret = crypto.randomUUID();
12
-
13
- function shouldAuthorizeApi() {
14
- const logins = getLogins();
15
- return !!process.env.OAUTH_AUTH || !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH);
16
- }
17
-
18
- function getTokenLifetime() {
19
- return process.env.TOKEN_LIFETIME || '1d';
20
- }
21
-
22
18
  function unauthorizedResponse(req, res, text) {
23
19
  // if (req.path == getExpressPath('/config/get-settings')) {
24
20
  // return res.json({});
@@ -30,11 +26,32 @@ function unauthorizedResponse(req, res, text) {
30
26
  }
31
27
 
32
28
  function authMiddleware(req, res, next) {
33
- const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', '/auth/login', '/stream'];
34
-
35
- if (!shouldAuthorizeApi()) {
29
+ const SKIP_AUTH_PATHS = [
30
+ '/config/get',
31
+ '/config/logout',
32
+ '/config/get-settings',
33
+ '/config/save-license-key',
34
+ '/auth/oauth-token',
35
+ '/auth/login',
36
+ '/auth/redirect',
37
+ '/stream',
38
+ 'storage/get-connections-for-login-page',
39
+ 'auth/get-providers',
40
+ '/connections/dblogin-web',
41
+ '/connections/dblogin-app',
42
+ '/connections/dblogin-auth',
43
+ '/connections/dblogin-auth-token',
44
+ ];
45
+
46
+ // console.log('********************* getAuthProvider()', getAuthProvider());
47
+
48
+ // const isAdminPage = req.headers['x-is-admin-page'] == 'true';
49
+
50
+ if (process.env.BASIC_AUTH) {
51
+ // API is not authorized for basic auth
36
52
  return next();
37
53
  }
54
+
38
55
  let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x));
39
56
 
40
57
  const authHeader = req.headers.authorization;
@@ -46,7 +63,7 @@ function authMiddleware(req, res, next) {
46
63
  }
47
64
  const token = authHeader.split(' ')[1];
48
65
  try {
49
- const decoded = jwt.verify(token, tokenSecret);
66
+ const decoded = jwt.verify(token, getTokenSecret());
50
67
  req.user = decoded;
51
68
  return next();
52
69
  } catch (err) {
@@ -63,106 +80,49 @@ function authMiddleware(req, res, next) {
63
80
  module.exports = {
64
81
  oauthToken_meta: true,
65
82
  async oauthToken(params) {
66
- const { redirectUri, code } = params;
67
-
68
- const scopeParam = process.env.OAUTH_SCOPE ? `&scope=${process.env.OAUTH_SCOPE}` : '';
69
- const resp = await axios.default.post(
70
- `${process.env.OAUTH_TOKEN}`,
71
- `grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(
72
- redirectUri
73
- )}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}${scopeParam}`
74
- );
75
-
76
- const { access_token, refresh_token } = resp.data;
77
-
78
- const payload = jwt.decode(access_token);
79
-
80
- logger.info({ payload }, 'User payload returned from OAUTH');
81
-
82
- const login =
83
- process.env.OAUTH_LOGIN_FIELD && payload && payload[process.env.OAUTH_LOGIN_FIELD]
84
- ? payload[process.env.OAUTH_LOGIN_FIELD]
85
- : 'oauth';
86
-
87
- if (
88
- process.env.OAUTH_ALLOWED_LOGINS &&
89
- !process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
90
- ) {
91
- return { error: `Username ${login} not allowed to log in` };
92
- }
93
-
94
- const groups =
95
- process.env.OAUTH_GROUP_FIELD && payload && payload[process.env.OAUTH_GROUP_FIELD]
96
- ? payload[process.env.OAUTH_GROUP_FIELD]
97
- : [];
98
-
99
- const allowedGroups =
100
- process.env.OAUTH_ALLOWED_GROUPS
101
- ? process.env.OAUTH_ALLOWED_GROUPS.split(',').map(group => group.toLowerCase().trim())
102
- : [];
103
-
104
- if (
105
- process.env.OAUTH_ALLOWED_GROUPS &&
106
- !groups.some(group => allowedGroups.includes(group.toLowerCase().trim()))
107
- ) {
108
- return { error: `Username ${login} does not belong to an allowed group` };
109
- }
110
-
111
- if (access_token) {
112
- return {
113
- accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
114
- };
115
- }
116
-
117
- return { error: 'Token not found' };
83
+ const { amoid } = params;
84
+ return getAuthProviderById(amoid).oauthToken(params);
118
85
  },
119
86
  login_meta: true,
120
87
  async login(params) {
121
- const { login, password } = params;
122
-
123
- if (process.env.AD_URL) {
124
- const adConfig = {
125
- url: process.env.AD_URL,
126
- baseDN: process.env.AD_BASEDN,
127
- username: process.env.AD_USERNAME,
128
- password: process.env.AD_PASSOWRD,
129
- };
130
- const ad = new AD(adConfig);
131
- try {
132
- const res = await ad.authenticate(login, password);
133
- if (!res) {
134
- return { error: 'Login failed' };
135
- }
136
- if (
137
- process.env.AD_ALLOWED_LOGINS &&
138
- !process.env.AD_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
139
- ) {
140
- return { error: `Username ${login} not allowed to log in` };
141
- }
142
- return {
143
- accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
144
- };
145
- } catch (err) {
146
- logger.error({ err }, 'Failed active directory authentization');
88
+ const { amoid, login, password, isAdminPage } = params;
89
+
90
+ if (isAdminPage) {
91
+ if (process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD == password) {
147
92
  return {
148
- error: err.message,
93
+ accessToken: jwt.sign(
94
+ {
95
+ login: 'superadmin',
96
+ permissions: await storage.loadSuperadminPermissions(),
97
+ roleId: -3,
98
+ },
99
+ getTokenSecret(),
100
+ {
101
+ expiresIn: getTokenLifetime(),
102
+ }
103
+ ),
149
104
  };
150
105
  }
151
- }
152
106
 
153
- const logins = getLogins();
154
- if (!logins) {
155
- return { error: 'Logins not configured' };
156
- }
157
- const foundLogin = logins.find(x => x.login == login);
158
- if (foundLogin && foundLogin.password && foundLogin.password == password) {
159
- return {
160
- accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
161
- };
107
+ return { error: 'Login failed' };
162
108
  }
163
- return { error: 'Invalid credentials' };
109
+
110
+ return getAuthProviderById(amoid).login(login, password);
111
+ },
112
+
113
+ getProviders_meta: true,
114
+ getProviders() {
115
+ return {
116
+ providers: getAuthProviders().map(x => x.toJson()),
117
+ default: getDefaultAuthProvider()?.amoid,
118
+ };
119
+ },
120
+
121
+ redirect_meta: true,
122
+ async redirect(params) {
123
+ const { amoid } = params;
124
+ return getAuthProviderById(amoid).redirect(params);
164
125
  },
165
126
 
166
127
  authMiddleware,
167
- shouldAuthorizeApi,
168
128
  };