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