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