@universis/janitor 2.1.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +101 -124
- package/dist/index.esm.js +588 -14
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +598 -12
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/NetworkRestrictAccess.d.ts +16 -0
- package/src/NetworkRestrictAccess.js +181 -0
- package/src/OnAccountUpdate.js +55 -0
- package/src/OutputCachingService.d.ts +7 -0
- package/src/OutputCachingService.js +47 -0
- package/src/RemoteAddressValidator.d.ts +6 -2
- package/src/UserWithNetworkAddress.d.ts +9 -0
- package/src/UserWithNetworkAddress.js +94 -0
- package/src/VerifyUserClientScope.d.ts +15 -0
- package/src/VerifyUserClientScope.js +87 -0
- package/src/config/models/AccountNetworkAddress.js +56 -0
- package/src/index.d.ts +7 -2
- package/src/index.js +5 -0
- package/src/outputCacheRouter.d.ts +55 -0
- package/src/outputCacheRouter.js +145 -0
- package/src/outputCaching.json +96 -0
- package/src/validateScope.d.ts +1 -0
- package/src/validateScope.js +42 -14
package/dist/index.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'core-js/modules/es.string.replace-all';
|
|
2
2
|
import { ApplicationService, TraceUtils, ConfigurationStrategy, Args, HttpForbiddenError, DataError, HttpError } from '@themost/common';
|
|
3
3
|
import { rateLimit } from 'express-rate-limit';
|
|
4
|
-
import express from 'express';
|
|
4
|
+
import express, { Router } from 'express';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { BehaviorSubject } from 'rxjs';
|
|
7
7
|
import slowDown from 'express-slow-down';
|
|
@@ -9,10 +9,17 @@ import { RedisStore } from 'rate-limit-redis';
|
|
|
9
9
|
import { Redis } from 'ioredis';
|
|
10
10
|
import '@themost/promise-sequence';
|
|
11
11
|
import url, { URL } from 'url';
|
|
12
|
+
import { ScopeString as ScopeString$1 } from '@universis/janitor';
|
|
13
|
+
import '@themost/express';
|
|
12
14
|
import { Request } from 'superagent';
|
|
13
15
|
import jwt from 'jsonwebtoken';
|
|
14
16
|
import BearerStrategy from 'passport-http-bearer';
|
|
15
17
|
import passport from 'passport';
|
|
18
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
19
|
+
import MD5 from 'crypto-js/md5';
|
|
20
|
+
import { ValueFormatter, UserService, SchemaLoaderStrategy } from '@themost/data';
|
|
21
|
+
import ipRangeCheck from 'ip-range-check';
|
|
22
|
+
import { take } from 'rxjs/operators';
|
|
16
23
|
|
|
17
24
|
class RateLimitService extends ApplicationService {
|
|
18
25
|
/**
|
|
@@ -802,23 +809,106 @@ class ExtendScopeAccessConfiguration extends ApplicationService {
|
|
|
802
809
|
}
|
|
803
810
|
}
|
|
804
811
|
|
|
805
|
-
|
|
806
|
-
|
|
812
|
+
class InvalidClientScopeError extends HttpForbiddenError {
|
|
813
|
+
constructor() {
|
|
814
|
+
super('Access denied due to invalid client scopes.');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
class VerifyUserClientScope extends ApplicationService {
|
|
819
|
+
|
|
820
|
+
constructor(app) {
|
|
821
|
+
super(app);
|
|
822
|
+
// Sets the pre-defined OpenID Connect client scopes that are whitelisted and should be omitted during validation
|
|
823
|
+
this.options = {
|
|
824
|
+
whitelist: [
|
|
825
|
+
'profile',
|
|
826
|
+
'email',
|
|
827
|
+
'openid']
|
|
828
|
+
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Verify user scope access against its assigned groups,
|
|
834
|
+
* following the client scopes that are available for the user groups.
|
|
835
|
+
* @param {ExpressDataContext} context
|
|
836
|
+
* @param {boolean=} throwError
|
|
837
|
+
* @returns {Promise<boolean>}
|
|
838
|
+
*/
|
|
839
|
+
async verify(context, throwError) {
|
|
840
|
+
// noinspection JSUnresolvedReference
|
|
841
|
+
const user = await context.getUser();
|
|
842
|
+
if (user == null) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
const groups = user.groups.map((group) => group.id);
|
|
846
|
+
/**
|
|
847
|
+
* @type {import('@themost/data').DataModel}
|
|
848
|
+
*/
|
|
849
|
+
const ClientScopes = context.model('Group').convert({}).property('scopes').getBaseModel();
|
|
850
|
+
// set conditional caching
|
|
851
|
+
ClientScopes.caching = 'conditional';
|
|
852
|
+
/**
|
|
853
|
+
* get client scopes assigned to user groups
|
|
854
|
+
* @type {Array<{ scope: string }>}
|
|
855
|
+
*/
|
|
856
|
+
const items = await ClientScopes.where('group').equal(groups).select('scope').cache(true).take(-1).silent().getItems();
|
|
807
857
|
/**
|
|
808
|
-
* @type {
|
|
858
|
+
* @type {string[]}
|
|
809
859
|
*/
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
return
|
|
860
|
+
const assignedScopes = items.map((item) => item.scope);
|
|
861
|
+
const contextScopes = new ScopeString$1(context.user.authenticationScope).split().filter((scope) => {
|
|
862
|
+
return this.options.whitelist.includes(scope) === false;
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const unassigned = contextScopes.filter((scope) => {
|
|
866
|
+
return !assignedScopes.includes(scope);
|
|
867
|
+
});
|
|
868
|
+
if (unassigned.length) {
|
|
869
|
+
if (!!throwError) {
|
|
870
|
+
TraceUtils.error(`Access denied for user "${context.user.name}" due to invalid client scope(s) "${unassigned.join(',')}"`);
|
|
871
|
+
throw new InvalidClientScopeError();
|
|
872
|
+
}
|
|
873
|
+
return false;
|
|
813
874
|
}
|
|
814
|
-
|
|
815
|
-
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
class DisableUserClientScopeVerification extends VerifyUserClientScope {
|
|
880
|
+
constructor(app) {
|
|
881
|
+
super(app);
|
|
882
|
+
}
|
|
883
|
+
async verify(context, throwError) {
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function validateClientScope() {
|
|
889
|
+
return function (req, res, next) {
|
|
890
|
+
try {
|
|
891
|
+
/**
|
|
892
|
+
* @type {VerifyUserClientScope}
|
|
893
|
+
*/
|
|
894
|
+
const service = req.context.getApplication().getService(VerifyUserClientScope);
|
|
895
|
+
if (service == null) {
|
|
816
896
|
return next();
|
|
817
897
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
898
|
+
void service.verify(req).then(() => {
|
|
899
|
+
return next();
|
|
900
|
+
}).catch((reason) => {
|
|
901
|
+
return next(reason);
|
|
902
|
+
});
|
|
903
|
+
} catch (err) {
|
|
904
|
+
return next(err);
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function validateScope() {
|
|
910
|
+
return (req, res, next) => {
|
|
911
|
+
return validateClientScope();
|
|
822
912
|
};
|
|
823
913
|
}
|
|
824
914
|
|
|
@@ -1335,5 +1425,489 @@ class PassportService extends ApplicationService {
|
|
|
1335
1425
|
|
|
1336
1426
|
}
|
|
1337
1427
|
|
|
1338
|
-
|
|
1428
|
+
/**
|
|
1429
|
+
*
|
|
1430
|
+
* @param {{profiles:Map<string, import('./outputCacheRouter').OutputCachingProfile>,elements: import('./outputCacheRouter').OutputCachingElement[]}} options
|
|
1431
|
+
* @returns {import('express').Router}
|
|
1432
|
+
*/
|
|
1433
|
+
function outputCache({ profiles, elements }) {
|
|
1434
|
+
const router = new Router();
|
|
1435
|
+
// prepare regular expressions
|
|
1436
|
+
const outputCacheElements = Array.isArray(elements) ? elements : [];
|
|
1437
|
+
outputCacheElements.forEach((element) => {
|
|
1438
|
+
if (!(element.expr instanceof RegExp)) {
|
|
1439
|
+
element.expr = new RegExp('^' + element.resource, 'g');
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
router.use((req, res, next) => {
|
|
1443
|
+
// only GET requests are cacheable
|
|
1444
|
+
if (req.method !== 'GET') {
|
|
1445
|
+
return next();
|
|
1446
|
+
}
|
|
1447
|
+
// if cache-control header is set to no-cache then skip caching
|
|
1448
|
+
const cacheControl = req.headers['cache-control'];
|
|
1449
|
+
if (cacheControl && cacheControl.includes('no-cache')) {
|
|
1450
|
+
return next();
|
|
1451
|
+
}
|
|
1452
|
+
for (let element of outputCacheElements) {
|
|
1453
|
+
if (element.expr.test(req.path)) {
|
|
1454
|
+
let profile;
|
|
1455
|
+
if (typeof element.profile === 'string') {
|
|
1456
|
+
profile = profiles.get(element.profile);
|
|
1457
|
+
} else if (typeof element.profile === 'object') {
|
|
1458
|
+
profile = element.profile;
|
|
1459
|
+
}
|
|
1460
|
+
if (profile) {
|
|
1461
|
+
// clone profile to avoid mutation
|
|
1462
|
+
Object.assign(req, {
|
|
1463
|
+
outputCaching: cloneDeep(profile)
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
return next();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return next();
|
|
1470
|
+
});
|
|
1471
|
+
router.use((req, res, next) => {
|
|
1472
|
+
try {
|
|
1473
|
+
const shouldCached = req.outputCaching && (req.outputCaching.location === 'any' || req.outputCaching.location === 'client');
|
|
1474
|
+
if (!shouldCached) {
|
|
1475
|
+
return next();
|
|
1476
|
+
}
|
|
1477
|
+
// determine cache location
|
|
1478
|
+
let cacheLocation = req.outputCaching.location === 'any' ? 'public' : 'private';
|
|
1479
|
+
if (req.headers.authorization && req.headers.authorization.length > 0) {
|
|
1480
|
+
// if authorization header is present, we should not cache the response publicly
|
|
1481
|
+
cacheLocation = 'private';
|
|
1482
|
+
}
|
|
1483
|
+
if (req.outputCaching.duration) {
|
|
1484
|
+
res.setHeader('Cache-Control', `${cacheLocation}, max-age=${req.outputCaching.duration}`);
|
|
1485
|
+
} else {
|
|
1486
|
+
res.setHeader('Cache-Control', cacheLocation);
|
|
1487
|
+
}
|
|
1488
|
+
const { varyByContentEncoding, varyByHeader, varyByParam, location, duration } = req.outputCaching;
|
|
1489
|
+
if (varyByContentEncoding) {
|
|
1490
|
+
res.setHeader('Vary', 'Accept-Encoding');
|
|
1491
|
+
}
|
|
1492
|
+
const varyByReqHeader = varyByHeader || [];
|
|
1493
|
+
if (varyByReqHeader.length) {
|
|
1494
|
+
res.setHeader('Vary', varyByReqHeader.join(', '));
|
|
1495
|
+
}
|
|
1496
|
+
let varyByQueryParam = varyByParam || [];
|
|
1497
|
+
if (varyByQueryParam.length === 1 && varyByQueryParam[0] === '*') {
|
|
1498
|
+
varyByQueryParam = Object.keys(req.query).sort((a, b) => {
|
|
1499
|
+
if (a < b) {
|
|
1500
|
+
return -1;
|
|
1501
|
+
}
|
|
1502
|
+
if (a > b) {
|
|
1503
|
+
return 1;
|
|
1504
|
+
}
|
|
1505
|
+
return 0;
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
const headers = varyByReqHeader.reduce((acc, val) => {
|
|
1510
|
+
acc[val] = req.get(val);
|
|
1511
|
+
return acc;
|
|
1512
|
+
}, {});
|
|
1513
|
+
|
|
1514
|
+
const params = varyByQueryParam.reduce((acc, val) => {
|
|
1515
|
+
if (req.query[val]) {
|
|
1516
|
+
acc[val] = req.query[val];
|
|
1517
|
+
}
|
|
1518
|
+
return acc;
|
|
1519
|
+
}, {});
|
|
1520
|
+
|
|
1521
|
+
// vary by query parameters is not supported by HTTP caching, but we can use it to create a cache key
|
|
1522
|
+
const entry = {
|
|
1523
|
+
path: req.path,
|
|
1524
|
+
location: location,
|
|
1525
|
+
contentEncoding: varyByContentEncoding,
|
|
1526
|
+
headers,
|
|
1527
|
+
params,
|
|
1528
|
+
customParams: null,
|
|
1529
|
+
duration: duration
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
(async () => {
|
|
1533
|
+
const { varyByExpr } = req.outputCaching;
|
|
1534
|
+
if (varyByExpr && typeof varyByExpr === 'object') {
|
|
1535
|
+
// noinspection JSCheckFunctionSignatures
|
|
1536
|
+
const formatter = new ValueFormatter(req.context, null, req);
|
|
1537
|
+
entry.customParams = await formatter.format(varyByExpr);
|
|
1538
|
+
}
|
|
1539
|
+
return `W/"${MD5(JSON.stringify({
|
|
1540
|
+
path: entry.path,
|
|
1541
|
+
location: entry.location,
|
|
1542
|
+
contentEncoding: entry.contentEncoding,
|
|
1543
|
+
headers: entry.headers,
|
|
1544
|
+
params: entry.params,
|
|
1545
|
+
customParams: entry.customParams,
|
|
1546
|
+
duration: entry.duration
|
|
1547
|
+
})).toString()}"`;
|
|
1548
|
+
})().then((entityTag) => {
|
|
1549
|
+
if (req.headers['if-none-match'] === entityTag) {
|
|
1550
|
+
return res.status(304).end();
|
|
1551
|
+
}
|
|
1552
|
+
res.setHeader('ETag', entityTag);
|
|
1553
|
+
res.setHeader('Date', new Date().toUTCString());
|
|
1554
|
+
return next();
|
|
1555
|
+
}).catch((err) => {
|
|
1556
|
+
return next(err);
|
|
1557
|
+
});
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
return next(err);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
return router;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
class OutputCachingService extends ApplicationService {
|
|
1566
|
+
constructor(app) {
|
|
1567
|
+
super(app);
|
|
1568
|
+
let options = app.getConfiguration().getSourceAt('settings/universis/outputCaching') || {
|
|
1569
|
+
profiles: [],
|
|
1570
|
+
elements: []
|
|
1571
|
+
};
|
|
1572
|
+
if (typeof options.extends === 'string') {
|
|
1573
|
+
const configurationDir = this.getApplication().getConfiguration().getConfigurationPath();
|
|
1574
|
+
const parentOptions = require(path.resolve(configurationDir, options.extends));
|
|
1575
|
+
options = Object.assign({}, parentOptions, options);
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* @type {Map<string, OutputCachingProfile>}
|
|
1579
|
+
*/
|
|
1580
|
+
this.profiles = new Map(options.profiles || []);
|
|
1581
|
+
/**
|
|
1582
|
+
* @type {OutputCachingElement[]}
|
|
1583
|
+
*/
|
|
1584
|
+
this.elements = options.elements || [];
|
|
1585
|
+
if (app && app.container) {
|
|
1586
|
+
app.container.subscribe((container) => {
|
|
1587
|
+
if (container == null) {
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
container.use(outputCache({
|
|
1591
|
+
profiles: this.profiles,
|
|
1592
|
+
elements: this.elements
|
|
1593
|
+
}));
|
|
1594
|
+
const stack = container._router && container._router.stack;
|
|
1595
|
+
if (Array.isArray(stack)) {
|
|
1596
|
+
let index = stack.findIndex((item) => item.name === 'dataContextMiddleware');
|
|
1597
|
+
if (index > -1) {
|
|
1598
|
+
stack.splice(index, 0, stack.pop());
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
class UserWithNetworkAddress extends UserService {
|
|
1607
|
+
constructor(app) {
|
|
1608
|
+
super(app);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* @protected
|
|
1613
|
+
* @param {import('@themost/data').DataContext} context
|
|
1614
|
+
* @param {string} name
|
|
1615
|
+
* @return {import('@themost/data').DataQueryable}
|
|
1616
|
+
*/
|
|
1617
|
+
queryUser(context, name) {
|
|
1618
|
+
return context.model('User').asQueryable().expand('groups', 'networkAddress').
|
|
1619
|
+
where('name').equal(name);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* @protected
|
|
1624
|
+
* @param {import('@themost/data').DataContext} context
|
|
1625
|
+
* @param {string} name
|
|
1626
|
+
* @return {import('@themost/data').DataQueryable}
|
|
1627
|
+
*/
|
|
1628
|
+
queryGroup(context, name) {
|
|
1629
|
+
return context.model('Group').asQueryable().expand('groups', 'networkAddress').
|
|
1630
|
+
where('name').equal(name);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Gets user by name
|
|
1635
|
+
* @param context
|
|
1636
|
+
* @param name
|
|
1637
|
+
* @returns {Promise<any>}
|
|
1638
|
+
*/
|
|
1639
|
+
getUser(context, name) {
|
|
1640
|
+
// noinspection JSCheckFunctionSignatures
|
|
1641
|
+
/**
|
|
1642
|
+
* @type {import('./data-cache').DataCacheStrategy}
|
|
1643
|
+
*/
|
|
1644
|
+
// noinspection JSCheckFunctionSignatures
|
|
1645
|
+
const cache = context.getConfiguration().getStrategy(function DataCacheStrategy() {});
|
|
1646
|
+
if (cache) {
|
|
1647
|
+
return cache.getOrDefault(`/User/${name}`, () => {
|
|
1648
|
+
return this.queryUser(context, name).silent().getItem().then((user) => {
|
|
1649
|
+
return user;
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
return this.queryUser(context, name).silent().getItem().then((user) => {
|
|
1654
|
+
return user;
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Gets group by name
|
|
1660
|
+
* @param context
|
|
1661
|
+
* @param name
|
|
1662
|
+
* @returns {Promise<any>}
|
|
1663
|
+
*/
|
|
1664
|
+
getGroup(context, name) {
|
|
1665
|
+
// noinspection JSCheckFunctionSignatures
|
|
1666
|
+
/**
|
|
1667
|
+
* @type {import('./data-cache').DataCacheStrategy}
|
|
1668
|
+
*/
|
|
1669
|
+
// noinspection JSCheckFunctionSignatures
|
|
1670
|
+
const cache = context.getConfiguration().getStrategy(function DataCacheStrategy() {});
|
|
1671
|
+
if (cache) {
|
|
1672
|
+
return cache.getOrDefault(`/Group/${name}`, () => {
|
|
1673
|
+
return this.queryGroup(context, name).silent().getItem().then((group) => {
|
|
1674
|
+
return group;
|
|
1675
|
+
});
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
return this.queryGroup(context, name).silent().getItem().then((group) => {
|
|
1679
|
+
return group;
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* Gets anonymous user
|
|
1685
|
+
* @param context
|
|
1686
|
+
* @returns {Promise<any>}
|
|
1687
|
+
*/
|
|
1688
|
+
getAnonymousUser(context) {
|
|
1689
|
+
return this.getUser(context, 'anonymous');
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
const AccountNetworkAddress = {
|
|
1695
|
+
"$schema": "https://themost-framework.github.io/themost/models/2018/2/schema.json",
|
|
1696
|
+
"@id": "https://universis.io/schemas/AccountNetworkAddress",
|
|
1697
|
+
"name": "AccountNetworkAddress",
|
|
1698
|
+
"hidden": true,
|
|
1699
|
+
"source": "AccountNetworkAddress",
|
|
1700
|
+
"view": "AccountNetworkAddress",
|
|
1701
|
+
"version": "1.0.0",
|
|
1702
|
+
"fields": [
|
|
1703
|
+
{
|
|
1704
|
+
"name": "id",
|
|
1705
|
+
"type": "Counter",
|
|
1706
|
+
"nullable": false,
|
|
1707
|
+
"primary": true,
|
|
1708
|
+
"many": false
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
"name": "account",
|
|
1712
|
+
"type": "Account",
|
|
1713
|
+
"nullable": false,
|
|
1714
|
+
"many": false,
|
|
1715
|
+
"indexed": true
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
"name": "value",
|
|
1719
|
+
"type": "Text",
|
|
1720
|
+
"nullable": false,
|
|
1721
|
+
"many": false,
|
|
1722
|
+
"indexed": false
|
|
1723
|
+
}],
|
|
1724
|
+
|
|
1725
|
+
"constraints": [
|
|
1726
|
+
{
|
|
1727
|
+
"type": "unique",
|
|
1728
|
+
"fields": [
|
|
1729
|
+
"account",
|
|
1730
|
+
"value"]
|
|
1731
|
+
|
|
1732
|
+
}],
|
|
1733
|
+
|
|
1734
|
+
"privileges": [
|
|
1735
|
+
{
|
|
1736
|
+
"mask": 15,
|
|
1737
|
+
"type": "global"
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
"mask": 15,
|
|
1741
|
+
"type": "global",
|
|
1742
|
+
"account": "Administrators"
|
|
1743
|
+
}]
|
|
1744
|
+
|
|
1745
|
+
};
|
|
1746
|
+
|
|
1747
|
+
class HttpNetworkAccessForbiddenError extends HttpForbiddenError {
|
|
1748
|
+
constructor() {
|
|
1749
|
+
super('Access is denied due to network address conflict.');
|
|
1750
|
+
this.statusCode = 403.6;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
class NetworkRestrictAccess extends ApplicationService {
|
|
1756
|
+
constructor(app) {
|
|
1757
|
+
super(app);
|
|
1758
|
+
Object.defineProperty(this, 'proxyAddressForwarding', {
|
|
1759
|
+
configurable: true,
|
|
1760
|
+
enumerable: false,
|
|
1761
|
+
value: !!app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding'),
|
|
1762
|
+
writable: false
|
|
1763
|
+
});
|
|
1764
|
+
this.apply();
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
apply() {
|
|
1768
|
+
// get schema loader
|
|
1769
|
+
const schemaLoader = this.getApplication().
|
|
1770
|
+
getConfiguration().
|
|
1771
|
+
getStrategy(SchemaLoaderStrategy);
|
|
1772
|
+
const accountNetworkAddressModel = schemaLoader.getModelDefinition('AccountNetworkAddress');
|
|
1773
|
+
if (accountNetworkAddressModel == null) {
|
|
1774
|
+
schemaLoader.setModelDefinition(AccountNetworkAddress);
|
|
1775
|
+
}
|
|
1776
|
+
// get model definition
|
|
1777
|
+
const model = schemaLoader.getModelDefinition('Account');
|
|
1778
|
+
if (model == null) {
|
|
1779
|
+
TraceUtils.warn('NetworkRestrictAccess: Account model definition not found.');
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
const found = model.fields.find((field) => {
|
|
1783
|
+
return field.name === 'networkAddress';
|
|
1784
|
+
});
|
|
1785
|
+
if (!found) {
|
|
1786
|
+
model.fields.push({
|
|
1787
|
+
"name": "networkAddress",
|
|
1788
|
+
"type": "Text",
|
|
1789
|
+
"many": true,
|
|
1790
|
+
"mapping": {
|
|
1791
|
+
"parentModel": "Account",
|
|
1792
|
+
"parentField": "id",
|
|
1793
|
+
"associationType": "junction",
|
|
1794
|
+
"associationAdapter": "AccountNetworkAddress",
|
|
1795
|
+
"cascade": "delete",
|
|
1796
|
+
"associationObjectField": "account",
|
|
1797
|
+
"associationValueField": "value"
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
model.eventListeners = model.eventListeners || [];
|
|
1802
|
+
let index = model.eventListeners.findIndex((listener) => {
|
|
1803
|
+
return listener.type === '@themost/data/previous-state-listener';
|
|
1804
|
+
});
|
|
1805
|
+
if (index < 0) {
|
|
1806
|
+
model.eventListeners.push({
|
|
1807
|
+
type: '@themost/data/previous-state-listener'
|
|
1808
|
+
});
|
|
1809
|
+
index = model.eventListeners.length - 1;
|
|
1810
|
+
}
|
|
1811
|
+
let otherIndex = model.eventListeners.findIndex((listener) => {
|
|
1812
|
+
return listener.type === path.resolve(__dirname, './OnAccountUpdate');
|
|
1813
|
+
});
|
|
1814
|
+
if (otherIndex < 0) {
|
|
1815
|
+
model.eventListeners.splice(index, 0, {
|
|
1816
|
+
type: path.resolve(__dirname, './OnAccountUpdate')
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
schemaLoader.setModelDefinition(model);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* Gets remote address from request
|
|
1824
|
+
* @param {import('express').Request} req
|
|
1825
|
+
* @returns
|
|
1826
|
+
*/
|
|
1827
|
+
getRemoteAddress(req) {
|
|
1828
|
+
let remoteAddress;
|
|
1829
|
+
if (this.proxyAddressForwarding) {
|
|
1830
|
+
// get proxy headers or remote address
|
|
1831
|
+
// noinspection JSDeprecatedSymbols
|
|
1832
|
+
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
|
|
1833
|
+
} else {
|
|
1834
|
+
// noinspection JSDeprecatedSymbols
|
|
1835
|
+
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
|
|
1836
|
+
}
|
|
1837
|
+
return remoteAddress;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* Validate network address for the current request against the assigned network addresses for the user account.
|
|
1845
|
+
* @returns {import('express').Handler}
|
|
1846
|
+
*/
|
|
1847
|
+
function validateNetworkAddress() {
|
|
1848
|
+
/**
|
|
1849
|
+
* @param {import('express').Request} req
|
|
1850
|
+
* @param {import('express').Response} res
|
|
1851
|
+
* @param {import('express').NextFunction} next
|
|
1852
|
+
*/
|
|
1853
|
+
return function (req, res, next) {
|
|
1854
|
+
try {
|
|
1855
|
+
const service = req.context.getApplication().getService(NetworkRestrictAccess);
|
|
1856
|
+
if (service == null) {
|
|
1857
|
+
return next();
|
|
1858
|
+
}
|
|
1859
|
+
req.context.user$.pipe(
|
|
1860
|
+
take(1)
|
|
1861
|
+
).subscribe({
|
|
1862
|
+
next: (user) => {
|
|
1863
|
+
if (user == null) {
|
|
1864
|
+
return next();
|
|
1865
|
+
}
|
|
1866
|
+
if (Array.isArray(user.networkAddress)) {
|
|
1867
|
+
if (user.networkAddress.length > 0) {
|
|
1868
|
+
// get remote address
|
|
1869
|
+
const remoteAddress = service.getRemoteAddress(req);
|
|
1870
|
+
const result = ipRangeCheck(remoteAddress, user.networkAddress);
|
|
1871
|
+
if (result !== true) {
|
|
1872
|
+
return next(new HttpNetworkAccessForbiddenError());
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return next();
|
|
1877
|
+
},
|
|
1878
|
+
error: (err) => {
|
|
1879
|
+
return next(err);
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
} catch (err) {
|
|
1883
|
+
return next(err);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
class EnableNetworkRestrictAccess extends NetworkRestrictAccess {
|
|
1890
|
+
constructor(app) {
|
|
1891
|
+
super(app);
|
|
1892
|
+
if (app && app.serviceRouter) {
|
|
1893
|
+
app.serviceRouter.subscribe((serviceRouter) => {
|
|
1894
|
+
if (serviceRouter == null) {
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const addRouter = express.Router();
|
|
1898
|
+
addRouter.use(validateNetworkAddress());
|
|
1899
|
+
// insert router at the beginning of serviceRouter.stack
|
|
1900
|
+
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
class DisableNetworkRestrictAccess extends NetworkRestrictAccess {
|
|
1907
|
+
constructor(app) {
|
|
1908
|
+
super(app);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
export { AppRateLimitService, AppSpeedLimitService, DefaultScopeAccessConfiguration, DisableNetworkRestrictAccess, DisableUserClientScopeVerification, EnableNetworkRestrictAccess, EnableScopeAccessConfiguration, ExtendScopeAccessConfiguration, HttpAccountDisabled, HttpBearerStrategy, HttpBearerTokenExpired, HttpBearerTokenNotFound, HttpBearerTokenRequired, HttpNetworkAccessForbiddenError, HttpRemoteAddrForbiddenError, InvalidClientScopeError, NetworkRestrictAccess, OAuth2ClientService, OutputCachingService, PassportService, RateLimitService, RedisClientStore, RemoteAddressValidator, ScopeAccessConfiguration, ScopeString, SpeedLimitService, UserWithNetworkAddress, VerifyUserClientScope, outputCache, validateClientScope, validateNetworkAddress, validateScope };
|
|
1339
1913
|
//# sourceMappingURL=index.esm.js.map
|