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