@universis/janitor 1.6.3 → 1.9.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.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/dist/index.d.ts +213 -7
  3. package/dist/index.esm.js +896 -0
  4. package/dist/index.esm.js.map +1 -0
  5. package/dist/index.js +909 -9
  6. package/dist/index.js.map +1 -1
  7. package/package.json +40 -20
  8. package/src/HttpBearerStategy.js +134 -0
  9. package/src/HttpBearerStrategy.d.ts +4 -0
  10. package/src/RedisClientStore.js +1 -1
  11. package/src/ScopeAccessConfiguration.d.ts +23 -1
  12. package/src/ScopeAccessConfiguration.js +38 -8
  13. package/src/index.d.ts +1 -1
  14. package/src/index.js +1 -1
  15. package/.gitlab-ci.yml +0 -29
  16. package/dist/OAuth2ClientService.d.ts +0 -98
  17. package/dist/OAuth2ClientService.js +0 -251
  18. package/dist/OAuth2ClientService.js.map +0 -1
  19. package/dist/RateLimitService.d.ts +0 -4
  20. package/dist/RateLimitService.js +0 -102
  21. package/dist/RateLimitService.js.map +0 -1
  22. package/dist/RedisClientStore.d.ts +0 -5
  23. package/dist/RedisClientStore.js +0 -122
  24. package/dist/RedisClientStore.js.map +0 -1
  25. package/dist/RemoteAddressValidator.d.ts +0 -10
  26. package/dist/RemoteAddressValidator.js +0 -89
  27. package/dist/RemoteAddressValidator.js.map +0 -1
  28. package/dist/ScopeAccessConfiguration.d.ts +0 -65
  29. package/dist/ScopeAccessConfiguration.js +0 -160
  30. package/dist/ScopeAccessConfiguration.js.map +0 -1
  31. package/dist/SpeedLimitService.d.ts +0 -4
  32. package/dist/SpeedLimitService.js +0 -113
  33. package/dist/SpeedLimitService.js.map +0 -1
  34. package/dist/polyfills.js +0 -11
  35. package/dist/polyfills.js.map +0 -1
  36. package/dist/validateScope.d.ts +0 -2
  37. package/dist/validateScope.js +0 -23
  38. package/dist/validateScope.js.map +0 -1
  39. package/src/polyfills.js +0 -10
package/dist/index.js CHANGED
@@ -1,9 +1,909 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", { value: true });require("./polyfills");
2
- var _RateLimitService = require("./RateLimitService");Object.keys(_RateLimitService).forEach(function (key) {if (key === "default" || key === "__esModule") return;if (key in exports && exports[key] === _RateLimitService[key]) return;Object.defineProperty(exports, key, { enumerable: true, get: function () {return _RateLimitService[key];} });});
3
- var _SpeedLimitService = require("./SpeedLimitService");Object.keys(_SpeedLimitService).forEach(function (key) {if (key === "default" || key === "__esModule") return;if (key in exports && exports[key] === _SpeedLimitService[key]) return;Object.defineProperty(exports, key, { enumerable: true, get: function () {return _SpeedLimitService[key];} });});
4
- var _RedisClientStore = require("./RedisClientStore");Object.keys(_RedisClientStore).forEach(function (key) {if (key === "default" || key === "__esModule") return;if (key in exports && exports[key] === _RedisClientStore[key]) return;Object.defineProperty(exports, key, { enumerable: true, get: function () {return _RedisClientStore[key];} });});
5
- var _ScopeAccessConfiguration = require("./ScopeAccessConfiguration");Object.keys(_ScopeAccessConfiguration).forEach(function (key) {if (key === "default" || key === "__esModule") return;if (key in exports && exports[key] === _ScopeAccessConfiguration[key]) return;Object.defineProperty(exports, key, { enumerable: true, get: function () {return _ScopeAccessConfiguration[key];} });});
6
- var _validateScope = require("./validateScope");Object.keys(_validateScope).forEach(function (key) {if (key === "default" || key === "__esModule") return;if (key in exports && exports[key] === _validateScope[key]) return;Object.defineProperty(exports, key, { enumerable: true, get: function () {return _validateScope[key];} });});
7
- var _OAuth2ClientService = require("./OAuth2ClientService");Object.keys(_OAuth2ClientService).forEach(function (key) {if (key === "default" || key === "__esModule") return;if (key in exports && exports[key] === _OAuth2ClientService[key]) return;Object.defineProperty(exports, key, { enumerable: true, get: function () {return _OAuth2ClientService[key];} });});
8
- var _RemoteAddressValidator = require("./RemoteAddressValidator");Object.keys(_RemoteAddressValidator).forEach(function (key) {if (key === "default" || key === "__esModule") return;if (key in exports && exports[key] === _RemoteAddressValidator[key]) return;Object.defineProperty(exports, key, { enumerable: true, get: function () {return _RemoteAddressValidator[key];} });});
9
- //# sourceMappingURL=index.js.map
1
+ 'use strict';
2
+
3
+ require('core-js/stable/string/replace-all');
4
+ var common = require('@themost/common');
5
+ var expressRateLimit = require('express-rate-limit');
6
+ var express = require('express');
7
+ var path = require('path');
8
+ var slowDown = require('express-slow-down');
9
+ var rateLimitRedis = require('rate-limit-redis');
10
+ var ioredis = require('ioredis');
11
+ require('@themost/promise-sequence');
12
+ var url = require('url');
13
+ var superagent = require('superagent');
14
+ var jwt = require('jsonwebtoken');
15
+
16
+ class RateLimitService extends common.ApplicationService {
17
+ /**
18
+ * @param {import('@themost/express').ExpressDataApplication} app
19
+ */
20
+ constructor(app) {
21
+ super(app);
22
+ app.serviceRouter.subscribe((serviceRouter) => {
23
+ if (serviceRouter == null) {
24
+ return;
25
+ }
26
+ try {
27
+ const addRouter = express.Router();
28
+ let serviceConfiguration = app.getConfiguration().getSourceAt('settings/universis/janitor/rateLimit') || {
29
+ profiles: [],
30
+ paths: []
31
+ };
32
+ if (serviceConfiguration.extends) {
33
+ // get additional configuration
34
+ const configurationPath = app.getConfiguration().getConfigurationPath();
35
+ const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
36
+ common.TraceUtils.log(`@universis/janitor#RateLimitService will try to extend service configuration from ${extendsPath}`);
37
+ serviceConfiguration = require(extendsPath);
38
+ }
39
+ const pathsArray = serviceConfiguration.paths || [];
40
+ const profilesArray = serviceConfiguration.profiles || [];
41
+ // create maps
42
+ const paths = new Map(pathsArray);
43
+ const profiles = new Map(profilesArray);
44
+ if (paths.size === 0) {
45
+ common.TraceUtils.warn('@universis/janitor#RateLimitService is being started but the collection of paths is empty.');
46
+ }
47
+ // get proxy address forwarding option
48
+ let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
49
+ if (typeof proxyAddressForwarding !== 'boolean') {
50
+ proxyAddressForwarding = false;
51
+ }
52
+ paths.forEach((value, path) => {
53
+ let profile;
54
+ // get profile
55
+ if (value.profile) {
56
+ profile = profiles.get(value.profile);
57
+ } else {
58
+ // or options defined inline
59
+ profile = value;
60
+ }
61
+ if (profile != null) {
62
+ const rateLimitOptions = Object.assign({
63
+ windowMs: 5 * 60 * 1000, // 5 minutes
64
+ limit: 50, // 50 requests
65
+ legacyHeaders: true // send headers
66
+ }, profile, {
67
+ keyGenerator: (req) => {
68
+ let remoteAddress;
69
+ if (proxyAddressForwarding) {
70
+ // get proxy headers or remote address
71
+ remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
72
+ } else {
73
+ // get remote address
74
+ remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
75
+ }
76
+ return `${path}:${remoteAddress}`;
77
+ }
78
+ });
79
+ if (typeof rateLimitOptions.store === 'string') {
80
+ // load store
81
+ const store = rateLimitOptions.store.split('#');
82
+ let StoreClass;
83
+ if (store.length === 2) {
84
+ const storeModule = require(store[0]);
85
+ if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
86
+ StoreClass = storeModule[store[1]];
87
+ rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
88
+ } else {
89
+ throw new Error(`${store} cannot be found or is inaccessible`);
90
+ }
91
+ } else {
92
+ StoreClass = require(store[0]);
93
+ // create store
94
+ rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
95
+ }
96
+ }
97
+ addRouter.use(path, expressRateLimit.rateLimit(rateLimitOptions));
98
+ }
99
+ });
100
+ if (addRouter.stack.length) {
101
+ serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
102
+ }
103
+ } catch (err) {
104
+ common.TraceUtils.error('An error occurred while validating rate limit configuration.');
105
+ common.TraceUtils.error(err);
106
+ common.TraceUtils.warn('Rate limit service is inactive due to an error occured while loading configuration.');
107
+ }
108
+ });
109
+ }
110
+
111
+ }
112
+
113
+ class SpeedLimitService extends common.ApplicationService {
114
+ constructor(app) {
115
+ super(app);
116
+
117
+ app.serviceRouter.subscribe((serviceRouter) => {
118
+ if (serviceRouter == null) {
119
+ return;
120
+ }
121
+ try {
122
+ const addRouter = express.Router();
123
+ let serviceConfiguration = app.getConfiguration().getSourceAt('settings/universis/janitor/speedLimit') || {
124
+ profiles: [],
125
+ paths: []
126
+ };
127
+ if (serviceConfiguration.extends) {
128
+ // get additional configuration
129
+ const configurationPath = app.getConfiguration().getConfigurationPath();
130
+ const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
131
+ common.TraceUtils.log(`@universis/janitor#SpeedLimitService will try to extend service configuration from ${extendsPath}`);
132
+ serviceConfiguration = require(extendsPath);
133
+ }
134
+ const pathsArray = serviceConfiguration.paths || [];
135
+ const profilesArray = serviceConfiguration.profiles || [];
136
+ // create maps
137
+ const paths = new Map(pathsArray);
138
+ const profiles = new Map(profilesArray);
139
+ if (paths.size === 0) {
140
+ common.TraceUtils.warn('@universis/janitor#SpeedLimitService is being started but the collection of paths is empty.');
141
+ }
142
+ // get proxy address forwarding option
143
+ let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
144
+ if (typeof proxyAddressForwarding !== 'boolean') {
145
+ proxyAddressForwarding = false;
146
+ }
147
+ paths.forEach((value, path) => {
148
+ let profile;
149
+ // get profile
150
+ if (value.profile) {
151
+ profile = profiles.get(value.profile);
152
+ } else {
153
+ // or options defined inline
154
+ profile = value;
155
+ }
156
+ if (profile != null) {
157
+ const slowDownOptions = Object.assign({
158
+ windowMs: 5 * 60 * 1000, // 5 minutes
159
+ delayAfter: 20, // 20 requests
160
+ delayMs: 500, // 500 ms
161
+ maxDelayMs: 10000 // 10 seconds
162
+ }, profile, {
163
+ keyGenerator: (req) => {
164
+ let remoteAddress;
165
+ if (proxyAddressForwarding) {
166
+ // get proxy headers or remote address
167
+ remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
168
+ } else {
169
+ // get remote address
170
+ remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
171
+ }
172
+ return `${path}:${remoteAddress}`;
173
+ }
174
+ });
175
+ if (Array.isArray(slowDownOptions.randomDelayMs)) {
176
+ slowDownOptions.delayMs = () => {
177
+ const delayMs = Math.floor(Math.random() * (slowDownOptions.randomDelayMs[1] - slowDownOptions.randomDelayMs[0] + 1) + slowDownOptions.randomDelayMs[0]);
178
+ return delayMs;
179
+ };
180
+ }
181
+ if (Array.isArray(slowDownOptions.randomMaxDelayMs)) {
182
+ slowDownOptions.maxDelayMs = () => {
183
+ const maxDelayMs = Math.floor(Math.random() * (slowDownOptions.randomMaxDelayMs[1] - slowDownOptions.randomMaxDelayMs[0] + 1) + slowDownOptions.randomMaxDelayMs[0]);
184
+ return maxDelayMs;
185
+ };
186
+ }
187
+ if (typeof slowDownOptions.store === 'string') {
188
+ // load store
189
+ const store = slowDownOptions.store.split('#');
190
+ let StoreClass;
191
+ if (store.length === 2) {
192
+ const storeModule = require(store[0]);
193
+ if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
194
+ StoreClass = storeModule[store[1]];
195
+ slowDownOptions.store = new StoreClass(this, slowDownOptions);
196
+ } else {
197
+ throw new Error(`${store} cannot be found or is inaccessible`);
198
+ }
199
+ } else {
200
+ StoreClass = require(store[0]);
201
+ // create store
202
+ slowDownOptions.store = new StoreClass(this, slowDownOptions);
203
+ }
204
+ }
205
+ addRouter.use(path, slowDown(slowDownOptions));
206
+ }
207
+ });
208
+ if (addRouter.stack.length) {
209
+ serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
210
+ }
211
+ } catch (err) {
212
+ common.TraceUtils.error('An error occurred while validating speed limit configuration.');
213
+ common.TraceUtils.error(err);
214
+ common.TraceUtils.warn('Speed limit service is inactive due to an error occured while loading configuration.');
215
+ }
216
+ });
217
+ }
218
+
219
+ }
220
+
221
+ function _defineProperty(e, r, t) {
222
+ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
223
+ value: t,
224
+ enumerable: true,
225
+ configurable: true,
226
+ writable: true
227
+ }) : e[r] = t, e;
228
+ }
229
+ function _toPrimitive(t, r) {
230
+ if ("object" != typeof t || !t) return t;
231
+ var e = t[Symbol.toPrimitive];
232
+ if (void 0 !== e) {
233
+ var i = e.call(t, r);
234
+ if ("object" != typeof i) return i;
235
+ throw new TypeError("@@toPrimitive must return a primitive value.");
236
+ }
237
+ return ("string" === r ? String : Number)(t);
238
+ }
239
+ function _toPropertyKey(t) {
240
+ var i = _toPrimitive(t, "string");
241
+ return "symbol" == typeof i ? i : i + "";
242
+ }
243
+
244
+ let superLoadIncrementScript;
245
+ let superLoadGetScript;
246
+
247
+ function noLoadGetScript() {
248
+
249
+ //
250
+ }
251
+ function noLoadIncrementScript() {
252
+
253
+ //
254
+ }
255
+ if (rateLimitRedis.RedisStore.prototype.loadIncrementScript.name === 'loadIncrementScript') {
256
+ // get super method for future use
257
+ superLoadIncrementScript = rateLimitRedis.RedisStore.prototype.loadIncrementScript;
258
+ rateLimitRedis.RedisStore.prototype.loadIncrementScript = noLoadIncrementScript;
259
+ }
260
+
261
+ if (rateLimitRedis.RedisStore.prototype.loadGetScript.name === 'loadGetScript') {
262
+ // get super method
263
+ superLoadGetScript = rateLimitRedis.RedisStore.prototype.loadGetScript;
264
+ rateLimitRedis.RedisStore.prototype.loadGetScript = noLoadGetScript;
265
+ }
266
+
267
+ class RedisClientStore extends rateLimitRedis.RedisStore {
268
+
269
+
270
+
271
+
272
+
273
+
274
+ /**
275
+ *
276
+ * @param {import('@themost/common').ApplicationService} service
277
+ * @param {{windowMs: number}} options
278
+ */
279
+ constructor(service, options) {
280
+ super({
281
+ /**
282
+ * @param {...string} args
283
+ * @returns {Promise<*>}
284
+ */
285
+ sendCommand: function () {
286
+ const args = Array.from(arguments);
287
+ const [command] = args.splice(0, 1);
288
+ const self = this;
289
+ if (command === 'SCRIPT') {
290
+ const connectOptions = service.getApplication().getConfiguration().getSourceAt('settings/redis/options') || {
291
+ host: '127.0.0.1',
292
+ port: 6379
293
+ };
294
+ const client = new ioredis.Redis(connectOptions);
295
+ return client.call(command, args).catch((error) => {
296
+ if (error instanceof TypeError && error.message === 'Invalid argument type') {
297
+ common.TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
298
+ }
299
+ return Promise.reject(error);
300
+ }).finally(() => {
301
+ if (client.isOpen) {
302
+ client.disconnect().catch((errDisconnect) => {
303
+ common.TraceUtils.error(errDisconnect);
304
+ });
305
+ }
306
+ });
307
+ }
308
+ if (self.client == null) {
309
+ const connectOptions = service.getApplication().getConfiguration().getSourceAt('settings/redis/options') || {
310
+ host: '127.0.0.1',
311
+ port: 6379
312
+ };
313
+ self.client = new ioredis.Redis(connectOptions);
314
+ }
315
+ if (self.client.isOpen) {
316
+ return self.client.call(command, args).catch((error) => {
317
+ if (error instanceof TypeError && error.message === 'Invalid argument type') {
318
+ common.TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
319
+ }
320
+ return Promise.reject(error);
321
+ });
322
+ }
323
+ // send load script commands once
324
+ return (() => {
325
+ if (self.incrementScriptSha == null) {
326
+ return this.postInit();
327
+ }
328
+ return Promise.resolve();
329
+ })().then(() => {
330
+ // send command
331
+ args[0] = self.incrementScriptSha;
332
+ return self.client.call(command, args).catch((error) => {
333
+ if (error instanceof TypeError && error.message === 'Invalid argument type') {
334
+ common.TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
335
+ }
336
+ return Promise.reject(error);
337
+ });
338
+ });
339
+ }
340
+ }); /**
341
+ * @type {import('redis').RedisClientType}
342
+ */_defineProperty(this, "client", void 0);this.init(options);common.TraceUtils.debug('RedisClientStore: Starting up and loading increment and get scripts.');
343
+ void this.postInit().then(() => {
344
+ common.TraceUtils.debug('RedisClientStore: Successfully loaded increment and get scripts.');
345
+ }).catch((err) => {
346
+ common.TraceUtils.error('RedisClientStore: Failed to load increment and get scripts.');
347
+ common.TraceUtils.error(err);
348
+ });
349
+ }
350
+ async postInit() {
351
+ const [incrementScriptSha, getScriptSha] = await Promise.sequence([
352
+ () => superLoadIncrementScript.call(this),
353
+ () => superLoadGetScript.call(this)]
354
+ );
355
+ this.incrementScriptSha = incrementScriptSha;
356
+ this.getScriptSha = getScriptSha;
357
+ }
358
+
359
+ }
360
+
361
+ const HTTP_METHOD_REGEXP = /^\b(POST|PUT|PATCH|DELETE)\b$/i;
362
+
363
+ class ScopeString {
364
+ constructor(str) {
365
+ this.value = str;
366
+ }
367
+ toString() {
368
+ return this.value;
369
+ }
370
+ /**
371
+ * Splits a comma-separated or space-separated scope string e.g. "profile email" or "profile,email"
372
+ *
373
+ * Important note: https://www.rfc-editor.org/rfc/rfc6749#section-3.3 defines the regular expression of access token scopes
374
+ * which is a space separated string. Several OAuth2 servers use a comma-separated list instead.
375
+ *
376
+ * The operation will try to use both implementations by excluding comma ',' from access token regular expressions
377
+ * @returns {Array<string>}
378
+ */
379
+ split() {
380
+ // the default regular expression includes comma /([\x21\x23-\x5B\x5D-\x7E]+)/g
381
+ // the modified regular expression excludes comma /x2C /([\x21\x23-\x2B\x2D-\x5B\x5D-\x7E]+)/g
382
+ const re = /([\x21\x23-\x2B\x2D-\x5B\x5D-\x7E]+)/g;
383
+ const results = [];
384
+ let match = re.exec(this.value);
385
+ while (match !== null) {
386
+ results.push(match[0]);
387
+ match = re.exec(this.value);
388
+ }
389
+ return results;
390
+ }
391
+ }
392
+
393
+ class ScopeAccessConfiguration extends common.ConfigurationStrategy {
394
+ /**
395
+ * @param {import('@themost/common').ConfigurationBase} configuration
396
+ */
397
+ constructor(configuration) {
398
+ super(configuration);
399
+ let elements = [];
400
+ // define property
401
+ Object.defineProperty(this, 'elements', {
402
+ get: () => {
403
+ return elements;
404
+ },
405
+ enumerable: true
406
+ });
407
+ }
408
+
409
+ /**
410
+ * @param {Request} req
411
+ * @returns Promise<ScopeAccessConfigurationElement>
412
+ */
413
+ verify(req) {
414
+ return new Promise((resolve, reject) => {
415
+ try {
416
+ // validate request context
417
+ common.Args.notNull(req.context, 'Context');
418
+ // validate request context user
419
+ common.Args.notNull(req.context.user, 'User');
420
+ if (req.context.user.authenticationScope && req.context.user.authenticationScope.length > 0) {
421
+ // get original url
422
+ let reqUrl = url.parse(req.originalUrl).pathname;
423
+ // get user context scopes as array e.g, ['students', 'students:read']
424
+ let reqScopes = new ScopeString(req.context.user.authenticationScope).split();
425
+ // get user access based on HTTP method e.g. GET -> read access
426
+ let reqAccess = HTTP_METHOD_REGEXP.test(req.method) ? 'write' : 'read';
427
+ // first phase: find element by resource and scope
428
+ let result = this.elements.find((x) => {
429
+ // filter element by access level
430
+ return new RegExp("^" + x.resource, 'i').test(reqUrl)
431
+ // and scopes
432
+ && x.scope.find((y) => {
433
+ // search user scopes (validate wildcard scope)
434
+ return y === "*" || reqScopes.indexOf(y) >= 0;
435
+ });
436
+ });
437
+ // second phase: check access level
438
+ if (result == null) {
439
+ return resolve();
440
+ }
441
+ // if access is missing or access is not an array
442
+ if (Array.isArray(result.access) === false) {
443
+ // the requested access is not allowed because the access is not defined
444
+ return resolve();
445
+ }
446
+ // if the requested access is not in the access array
447
+ if (result.access.indexOf(reqAccess) < 0) {
448
+ // the requested access is not allowed because the access levels do not match
449
+ return resolve();
450
+ }
451
+ // otherwise, return result
452
+ return resolve(result);
453
+ }
454
+ return resolve();
455
+ }
456
+ catch (err) {
457
+ return reject(err);
458
+ }
459
+
460
+ });
461
+ }
462
+
463
+ }
464
+
465
+ /**
466
+ * @class
467
+ */
468
+ class DefaultScopeAccessConfiguration extends ScopeAccessConfiguration {
469
+ /**
470
+ * @param {import('@themost/common').ConfigurationBase} configuration
471
+ */
472
+ constructor(configuration) {
473
+ super(configuration);
474
+ let defaults = [];
475
+ // load scope access from configuration resource
476
+ try {
477
+ /**
478
+ * @type {Array<ScopeAccessConfigurationElement>}
479
+ */
480
+ defaults = require(path.resolve(configuration.getConfigurationPath(), 'scope.access.json'));
481
+ }
482
+ catch (err) {
483
+ // if an error occurred other than module not found (there are no default access policies)
484
+ if (err.code !== 'MODULE_NOT_FOUND') {
485
+ // throw error
486
+ throw err;
487
+ }
488
+ // otherwise continue
489
+ }
490
+ this.elements.push.apply(this.elements, defaults);
491
+ }
492
+ }
493
+
494
+ class EnableScopeAccessConfiguration extends common.ApplicationService {
495
+ /**
496
+ * @param {import('@themost/express').ExpressDataApplication|import('@themost/common').ApplicationBase} app
497
+ */
498
+ constructor(app) {
499
+ super(app);
500
+ // register scope access configuration
501
+ app.getConfiguration().useStrategy(ScopeAccessConfiguration, DefaultScopeAccessConfiguration);
502
+ }
503
+ }
504
+
505
+
506
+ /**
507
+ * @class
508
+ */
509
+ class ExtendScopeAccessConfiguration extends common.ApplicationService {
510
+ /**
511
+ * @param {import('@themost/express').ExpressDataApplication|import('@themost/common').ApplicationBase} app
512
+ */
513
+ constructor(app) {
514
+ super(app);
515
+ // Get the additional scope access extensions from the configuration
516
+ const scopeAccessExtensions = app.getConfiguration().settings.universis?.janitor?.scopeAccess.imports;
517
+ if (app && app.container && scopeAccessExtensions != null) {
518
+ app.container.subscribe((container) => {
519
+ if (container) {
520
+ const scopeAccess = app.getConfiguration().getStrategy(function ScopeAccessConfiguration() {});
521
+ if (scopeAccess != null) {
522
+ for (const scopeAccessExtension of scopeAccessExtensions) {
523
+ try {
524
+ const elements = require(path.resolve(app.getConfiguration().getConfigurationPath(), scopeAccessExtension));
525
+ if (elements) {
526
+ // add extra scope access elements
527
+ scopeAccess.elements.unshift(...elements);
528
+ }
529
+ }
530
+ catch (err) {
531
+ // if an error occurred other than module not found (there are no default access policies)
532
+ if (err.code !== 'MODULE_NOT_FOUND') {
533
+ // throw error
534
+ throw err;
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ });
541
+ }
542
+ }
543
+ }
544
+
545
+ function validateScope() {
546
+ return (req, res, next) => {
547
+ /**
548
+ * @type {ScopeAccessConfiguration}
549
+ */
550
+ let scopeAccessConfiguration = req.context.getApplication().getConfiguration().getStrategy(ScopeAccessConfiguration);
551
+ if (typeof scopeAccessConfiguration === 'undefined') {
552
+ return next(new Error('Invalid application configuration. Scope access configuration strategy is missing or is in accessible.'));
553
+ }
554
+ scopeAccessConfiguration.verify(req).then((value) => {
555
+ if (value) {
556
+ return next();
557
+ }
558
+ return next(new common.HttpForbiddenError('Access denied due to authorization scopes.'));
559
+ }).catch((reason) => {
560
+ return next(reason);
561
+ });
562
+ };
563
+ }
564
+
565
+ function responseHander(resolve, reject) {
566
+ return function (err, response) {
567
+ if (err) {
568
+ /**
569
+ * @type {import('superagent').Response}
570
+ */
571
+ const response = err.response;
572
+ if (response && response.headers['content-type'] === 'application/json') {
573
+ // get body
574
+ const clientError = response.body;
575
+ const error = new common.HttpError(response.status);
576
+ return reject(Object.assign(error, {
577
+ clientError
578
+ }));
579
+ }
580
+ return reject(err);
581
+ }
582
+ if (response.status === 204 && response.headers['content-type'] === 'application/json') {
583
+ return resolve(null);
584
+ }
585
+ return resolve(response.body);
586
+ };
587
+ }
588
+
589
+ /**
590
+ * @class
591
+ */
592
+ class OAuth2ClientService extends common.ApplicationService {
593
+ /**
594
+ * @param {import('@themost/express').ExpressDataApplication} app
595
+ */
596
+ constructor(app) {
597
+ super(app);
598
+ /**
599
+ * @name OAuth2ClientService#settings
600
+ * @type {{server_uri:string,token_uri?:string}}
601
+ */
602
+ Object.defineProperty(this, 'settings', {
603
+ writable: false,
604
+ value: app.getConfiguration().getSourceAt('settings/auth'),
605
+ enumerable: false,
606
+ configurable: false
607
+ });
608
+ }
609
+
610
+ /**
611
+ * Gets keycloak server root
612
+ * @returns {string}
613
+ */
614
+ getServer() {
615
+ return this.settings.server_uri;
616
+ }
617
+
618
+ /**
619
+ * Gets keycloak server root
620
+ * @returns {string}
621
+ */
622
+ getAdminRoot() {
623
+ return this.settings.admin_uri;
624
+ }
625
+
626
+ // noinspection JSUnusedGlobalSymbols
627
+ /**
628
+ * Gets user's profile by calling OAuth2 server profile endpoint
629
+ * @param {ExpressDataContext} context
630
+ * @param {string} token
631
+ */
632
+ getUserInfo(token) {
633
+ return new Promise((resolve, reject) => {
634
+ const userinfo_uri = this.settings.userinfo_uri ? new url.URL(this.settings.userinfo_uri, this.getServer()) : new url.URL('me', this.getServer());
635
+ return new superagent.Request('GET', userinfo_uri).
636
+ set({
637
+ 'Authorization': `Bearer ${token}`,
638
+ 'Accept': 'application/json'
639
+ }).
640
+ query({
641
+ 'access_token': token
642
+ }).end(responseHander(resolve, reject));
643
+ });
644
+ }
645
+
646
+ // noinspection JSUnusedGlobalSymbols
647
+ /**
648
+ * Gets the token info of the current context
649
+ * @param {ExpressDataContext} context
650
+ */
651
+ getContextTokenInfo(context) {
652
+ if (context.user == null) {
653
+ return Promise.reject(new Error('Context user may not be null'));
654
+ }
655
+ if (context.user.authenticationType !== 'Bearer') {
656
+ return Promise.reject(new Error('Invalid context authentication type'));
657
+ }
658
+ if (context.user.authenticationToken == null) {
659
+ return Promise.reject(new Error('Context authentication data may not be null'));
660
+ }
661
+ return this.getTokenInfo(context, context.user.authenticationToken);
662
+ }
663
+ /**
664
+ * Gets token info by calling OAuth2 server endpoint
665
+ * @param {ExpressDataContext} _context
666
+ * @param {string} token
667
+ */
668
+ getTokenInfo(_context, token) {
669
+ return new Promise((resolve, reject) => {
670
+ const introspection_uri = this.settings.introspection_uri ? new url.URL(this.settings.introspection_uri, this.getServer()) : new url.URL('tokeninfo', this.getServer());
671
+ return new superagent.Request('POST', introspection_uri).
672
+ auth(this.settings.client_id, this.settings.client_secret).
673
+ set('Accept', 'application/json').
674
+ type('form').
675
+ send({
676
+ 'token_type_hint': 'access_token',
677
+ 'token': token
678
+ }).end(responseHander(resolve, reject));
679
+ });
680
+ }
681
+
682
+ /**
683
+ * @param {AuthorizeUser} authorizeUser
684
+ */
685
+ authorize(authorizeUser) {
686
+ const tokenURL = this.settings.token_uri ? new url.URL(this.settings.token_uri) : new url.URL('authorize', this.getServer());
687
+ return new Promise((resolve, reject) => {
688
+ return new superagent.Request('POST', tokenURL).
689
+ type('form').
690
+ send(authorizeUser).end(responseHander(resolve, reject));
691
+ });
692
+ }
693
+
694
+ /**
695
+ * Gets a user by name
696
+ * @param {*} user_id
697
+ * @param {AdminMethodOptions} options
698
+ */
699
+ getUserById(user_id, options) {
700
+ return new Promise((resolve, reject) => {
701
+ return new superagent.Request('GET', new url.URL(`users/${user_id}`, this.getAdminRoot())).
702
+ set('Authorization', `Bearer ${options.access_token}`).
703
+ end(responseHander(resolve, reject));
704
+ });
705
+ }
706
+
707
+ /**
708
+ * Gets a user by name
709
+ * @param {string} username
710
+ * @param {AdminMethodOptions} options
711
+ */
712
+ getUser(username, options) {
713
+ return new Promise((resolve, reject) => {
714
+ return new superagent.Request('GET', new url.URL('users', this.getAdminRoot())).
715
+ set('Authorization', `Bearer ${options.access_token}`).
716
+ query({
717
+ '$filter': `name eq '${username}'`
718
+ }).
719
+ end(responseHander(resolve, reject));
720
+ });
721
+ }
722
+
723
+ /**
724
+ * Gets a user by email address
725
+ * @param {string} email
726
+ * @param {AdminMethodOptions} options
727
+ */
728
+ getUserByEmail(email, options) {
729
+ return new Promise((resolve, reject) => {
730
+ return new superagent.Request('GET', new url.URL('users', this.getAdminRoot())).
731
+ set('Authorization', `Bearer ${options.access_token}`).
732
+ query({
733
+ '$filter': `alternateName eq '${email}'`
734
+ }).
735
+ end(responseHander(resolve, reject));
736
+ });
737
+ }
738
+
739
+ /**
740
+ * Updates an existing user
741
+ * @param {*} user
742
+ * @param {AdminMethodOptions} options
743
+ */
744
+ updateUser(user, options) {
745
+ return new Promise((resolve, reject) => {
746
+ if (user.id == null) {
747
+ return reject(new common.DataError('E_IDENTIFIER', 'User may not be empty at this context.', null, 'User', 'id'));
748
+ }
749
+ const request = new superagent.Request('PUT', new url.URL(`users/${user.id}`, this.getAdminRoot()));
750
+ return request.set('Authorization', `Bearer ${options.access_token}`).
751
+ set('Content-Type', 'application/json').
752
+ send(user).
753
+ end(responseHander(resolve, reject));
754
+ });
755
+ }
756
+
757
+ /**
758
+ * Creates a new user
759
+ * @param {*} user
760
+ * @param {AdminMethodOptions} options
761
+ */
762
+ createUser(user, options) {
763
+ return new Promise((resolve, reject) => {
764
+ const request = new superagent.Request('POST', new url.URL('users', this.getAdminRoot()));
765
+ return request.set('Authorization', `Bearer ${options.access_token}`).
766
+ set('Content-Type', 'application/json').
767
+ send(Object.assign({}, user, {
768
+ $state: 1 // for create
769
+ })).
770
+ end(responseHander(resolve, reject));
771
+ });
772
+ }
773
+
774
+ /**
775
+ * Deletes a user
776
+ * @param {{id: any}} user
777
+ * @param {AdminMethodOptions} options
778
+ */
779
+ deleteUser(user, options) {
780
+ return new Promise((resolve, reject) => {
781
+ if (user.id == null) {
782
+ return reject(new common.DataError('E_IDENTIFIER', 'User may not be empty at this context.', null, 'User', 'id'));
783
+ }
784
+ const request = new superagent.Request('DELETE', new url.URL(`users/${user.id}`, this.getAdminRoot()));
785
+ return request.set('Authorization', `Bearer ${options.access_token}`).
786
+ end(responseHander(resolve, reject));
787
+ });
788
+ }
789
+
790
+ /**
791
+ * @param {boolean=} force
792
+ * @returns {*}
793
+ */
794
+ getWellKnownConfiguration(force) {
795
+ if (force) {
796
+ this.well_known_configuration = null;
797
+ }
798
+ if (this.well_known_configuration) {
799
+ return Promise.resolve(this.well_known_configuration);
800
+ }
801
+ return new Promise((resolve, reject) => {
802
+ const well_known_configuration_uri = this.settings.well_known_configuration_uri ? new url.URL(this.settings.well_known_configuration_uri, this.getServer()) : new url.URL('.well-known/openid-configuration', this.getServer());
803
+ return new superagent.Request('GET', well_known_configuration_uri).
804
+ end(responseHander(resolve, reject));
805
+ }).then((configuration) => {
806
+ this.well_known_configuration = configuration;
807
+ return configuration;
808
+ });
809
+ }
810
+ }
811
+
812
+ class HttpRemoteAddrForbiddenError extends common.HttpForbiddenError {
813
+ constructor() {
814
+ super('Access is denied due to remote address conflict. The client network has been changed or cannot be determined.');
815
+ this.statusCode = 403.6;
816
+ }
817
+
818
+ }
819
+
820
+ class RemoteAddressValidator extends common.ApplicationService {
821
+ constructor(app) {
822
+ super(app);
823
+
824
+ // get proxy address forwarding option
825
+ let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
826
+ if (typeof proxyAddressForwarding !== 'boolean') {
827
+ proxyAddressForwarding = false;
828
+ }
829
+ this.proxyAddressForwarding = proxyAddressForwarding;
830
+ // get token claim name
831
+ this.claim = app.getConfiguration().getSourceAt('settings/universis/janitor/remoteAddress/claim') || 'remoteAddress';
832
+
833
+ app.serviceRouter.subscribe((serviceRouter) => {
834
+ if (serviceRouter == null) {
835
+ return;
836
+ }
837
+ const addRouter = express.Router();
838
+ addRouter.use((req, res, next) => {
839
+ void this.validateRemoteAddress(req).then((value) => {
840
+ if (value === false) {
841
+ return next(new HttpRemoteAddrForbiddenError());
842
+ }
843
+ return next();
844
+ }).catch((err) => {
845
+ return next(err);
846
+ });
847
+ });
848
+ // insert router at the beginning of serviceRouter.stack
849
+ serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
850
+ });
851
+ }
852
+
853
+ /**
854
+ * Gets remote address from request
855
+ * @param {import('express').Request} req
856
+ * @returns
857
+ */
858
+ getRemoteAddress(req) {
859
+ let remoteAddress;
860
+ if (this.proxyAddressForwarding) {
861
+ // get proxy headers or remote address
862
+ remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
863
+ } else {
864
+ remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
865
+ }
866
+ return remoteAddress;
867
+ }
868
+
869
+ /**
870
+ * Validates token remote address with request remote address
871
+ * @param {import('express').Request} req
872
+ * @returns {Promise<boolean>}
873
+ */
874
+ async validateRemoteAddress(req) {
875
+ const authenticationToken = req.context?.user?.authenticationToken;
876
+ if (authenticationToken != null) {
877
+ const access_token = jwt.decode(authenticationToken);
878
+ const remoteAddress = access_token[this.claim];
879
+ if (remoteAddress == null) {
880
+ common.TraceUtils.warn(`Remote address validation failed. Expected a valid remote address claimed by using "${this.claim}" attribute but got none.`);
881
+ return false;
882
+ }
883
+ // get context remote address
884
+ const requestRemoteAddress = this.getRemoteAddress(req);
885
+ if (remoteAddress !== requestRemoteAddress) {
886
+ common.TraceUtils.warn(`Remote address validation failed. Expected remote address is ${remoteAddress || 'Uknown'} but request remote address is ${requestRemoteAddress}`);
887
+ return false;
888
+ }
889
+ return true;
890
+ }
891
+ common.TraceUtils.warn('Remote address validation cannot be completed because authentication token is not available.');
892
+ return false;
893
+ }
894
+
895
+ }
896
+
897
+ exports.DefaultScopeAccessConfiguration = DefaultScopeAccessConfiguration;
898
+ exports.EnableScopeAccessConfiguration = EnableScopeAccessConfiguration;
899
+ exports.ExtendScopeAccessConfiguration = ExtendScopeAccessConfiguration;
900
+ exports.HttpRemoteAddrForbiddenError = HttpRemoteAddrForbiddenError;
901
+ exports.OAuth2ClientService = OAuth2ClientService;
902
+ exports.RateLimitService = RateLimitService;
903
+ exports.RedisClientStore = RedisClientStore;
904
+ exports.RemoteAddressValidator = RemoteAddressValidator;
905
+ exports.ScopeAccessConfiguration = ScopeAccessConfiguration;
906
+ exports.ScopeString = ScopeString;
907
+ exports.SpeedLimitService = SpeedLimitService;
908
+ exports.validateScope = validateScope;
909
+ //# sourceMappingURL=index.js.map