soajs.controller 4.3.3 → 4.3.5
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/Gruntfile.js +1 -1
- package/docs/configuration-custom-registry.md +149 -9
- package/docs/middleware/cache.md +237 -0
- package/docs/middleware/idempotency.md +209 -0
- package/docs/request-flow.md +35 -4
- package/lib/request.js +5 -0
- package/mw/cache/index.js +150 -0
- package/mw/cache/model/memory.js +117 -0
- package/mw/cache/model/mongo.js +98 -0
- package/mw/gotoService/redirectToService.js +77 -16
- package/mw/idempotency/index.js +171 -0
- package/mw/idempotency/model/memory.js +128 -0
- package/mw/idempotency/model/mongo.js +116 -0
- package/mw/mt/lib.js +17 -17
- package/mw/soajsRoutes/keyACL/index.js +1 -1
- package/package.json +3 -3
- package/server/controller.js +14 -0
- package/test/integration/UC4-multiuser-acl/readme.md +11 -0
- package/test/unit/index.js +4 -0
- package/test/unit/mw/cache/index.js +461 -0
- package/test/unit/mw/cache/memory.js +143 -0
- package/test/unit/mw/idempotency/index.js +354 -0
- package/test/unit/mw/idempotency/memory.js +151 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
|
|
12
|
+
const memory = require('./model/memory.js');
|
|
13
|
+
const mongo = require('./model/mongo.js');
|
|
14
|
+
|
|
15
|
+
let model = { memory, mongo };
|
|
16
|
+
|
|
17
|
+
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
18
|
+
|
|
19
|
+
function isValidUUIDv4(str) {
|
|
20
|
+
return UUID_V4_REGEX.test(str);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchesApi(method, path, apis) {
|
|
24
|
+
if (!apis || !Array.isArray(apis)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const requestSignature = `${method} ${path}`;
|
|
28
|
+
for (let api of apis) {
|
|
29
|
+
if (api === requestSignature) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const [apiMethod, apiPath] = api.split(' ');
|
|
33
|
+
if (apiMethod !== method) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const apiParts = apiPath.split('/');
|
|
37
|
+
const pathParts = path.split('/');
|
|
38
|
+
if (apiParts.length !== pathParts.length) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let matches = true;
|
|
42
|
+
for (let i = 0; i < apiParts.length; i++) {
|
|
43
|
+
if (apiParts[i].startsWith(':')) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (apiParts[i] !== pathParts[i]) {
|
|
47
|
+
matches = false;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (matches) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = function (configuration) {
|
|
59
|
+
model.memory.init(configuration.log);
|
|
60
|
+
model.mongo.init(configuration.log, configuration.gatewayDB);
|
|
61
|
+
|
|
62
|
+
return (req, res, next) => {
|
|
63
|
+
const idempotencyKey = req.headers['idempotency-key'];
|
|
64
|
+
|
|
65
|
+
if (!idempotencyKey) {
|
|
66
|
+
return next();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isValidUUIDv4(idempotencyKey)) {
|
|
70
|
+
req.soajs.controllerResponse({
|
|
71
|
+
'status': 400,
|
|
72
|
+
'code': 180,
|
|
73
|
+
'msg': 'Invalid Idempotency-Key format. Expected UUID v4.'
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!req.soajs || !req.soajs.tenant || !req.soajs.controller || !req.soajs.controller.serviceParams) {
|
|
79
|
+
return next();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const serviceName = req.soajs.controller.serviceParams.name;
|
|
83
|
+
// Strip service prefix from pathname to get API path (e.g., /console/registry -> /registry)
|
|
84
|
+
const serviceInfo = req.soajs.controller.serviceParams.serviceInfo;
|
|
85
|
+
const apiPath = "/" + serviceInfo.slice(2).join("/");
|
|
86
|
+
const method = req.method.toUpperCase();
|
|
87
|
+
|
|
88
|
+
if (method === 'GET') {
|
|
89
|
+
return next();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const idempotencyConfig = get(["soajs", "registry", "custom", "gateway", "value", "idempotency"], req);
|
|
93
|
+
if (!idempotencyConfig) {
|
|
94
|
+
return next();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const serviceConfig = idempotencyConfig.services && idempotencyConfig.services[serviceName];
|
|
98
|
+
if (!serviceConfig || !serviceConfig.enabled) {
|
|
99
|
+
return next();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (serviceConfig.apis && !matchesApi(method, apiPath, serviceConfig.apis)) {
|
|
103
|
+
return next();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const modelType = idempotencyConfig.model || 'memory';
|
|
107
|
+
if (!model[modelType]) {
|
|
108
|
+
configuration.log.warn('Idempotency: Unknown model [' + modelType + ']. It can only be [memory || mongo]');
|
|
109
|
+
return next();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const ttl = serviceConfig.ttl || 60000;
|
|
113
|
+
const key = {
|
|
114
|
+
'l1': req.soajs.tenant.id,
|
|
115
|
+
'l2': idempotencyKey
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const processIdempotency = async () => {
|
|
119
|
+
try {
|
|
120
|
+
const existing = await model[modelType].get(key);
|
|
121
|
+
|
|
122
|
+
if (existing) {
|
|
123
|
+
if (existing.status === 'in_flight') {
|
|
124
|
+
req.soajs.controllerResponse({
|
|
125
|
+
'status': 409,
|
|
126
|
+
'code': 181,
|
|
127
|
+
'msg': 'Request with this Idempotency-Key is still being processed.'
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (existing.status === 'completed' && existing.response) {
|
|
133
|
+
res.writeHead(existing.response.statusCode, existing.response.headers);
|
|
134
|
+
res.end(existing.response.body);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const locked = await model[modelType].lock(key, ttl);
|
|
140
|
+
if (!locked) {
|
|
141
|
+
req.soajs.controllerResponse({
|
|
142
|
+
'status': 409,
|
|
143
|
+
'code': 181,
|
|
144
|
+
'msg': 'Request with this Idempotency-Key is still being processed.'
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Store idempotency context for gotoService to use
|
|
150
|
+
req.soajs.idempotencyContext = {
|
|
151
|
+
key: key,
|
|
152
|
+
ttl: ttl,
|
|
153
|
+
modelType: modelType,
|
|
154
|
+
model: model[modelType],
|
|
155
|
+
log: configuration.log
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
next();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
configuration.log.error('Idempotency error:', err);
|
|
161
|
+
model[modelType].unlock(key).catch(() => {});
|
|
162
|
+
next();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
processIdempotency();
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Export model for external use
|
|
171
|
+
module.exports.model = model;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let dataHolder = {};
|
|
12
|
+
let cleanupInterval = null;
|
|
13
|
+
|
|
14
|
+
let model = {
|
|
15
|
+
'log': null,
|
|
16
|
+
|
|
17
|
+
'init': (log) => {
|
|
18
|
+
model.log = log;
|
|
19
|
+
dataHolder = {};
|
|
20
|
+
|
|
21
|
+
if (cleanupInterval) {
|
|
22
|
+
clearInterval(cleanupInterval);
|
|
23
|
+
}
|
|
24
|
+
const CLEANUP_INTERVAL = process.env.SOAJS_IDEMPOTENCY_CLEANUP_INTERVAL || 60000;
|
|
25
|
+
cleanupInterval = setInterval(() => {
|
|
26
|
+
model.cleanupExpiredEntries();
|
|
27
|
+
}, CLEANUP_INTERVAL);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
'cleanupExpiredEntries': () => {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
let removedCount = 0;
|
|
33
|
+
|
|
34
|
+
for (const l1Key in dataHolder) {
|
|
35
|
+
if (dataHolder.hasOwnProperty(l1Key)) {
|
|
36
|
+
const tenantData = dataHolder[l1Key];
|
|
37
|
+
|
|
38
|
+
for (const l2Key in tenantData) {
|
|
39
|
+
if (tenantData.hasOwnProperty(l2Key)) {
|
|
40
|
+
const entry = tenantData[l2Key];
|
|
41
|
+
if (entry.expiresAt && now > entry.expiresAt) {
|
|
42
|
+
delete tenantData[l2Key];
|
|
43
|
+
removedCount++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Object.keys(tenantData).length === 0) {
|
|
49
|
+
delete dataHolder[l1Key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (removedCount > 0 && model.log) {
|
|
55
|
+
model.log.debug(`Idempotency cache cleanup: removed ${removedCount} expired entries`);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
'get': (key) => {
|
|
60
|
+
if (!dataHolder[key.l1]) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const entry = dataHolder[key.l1][key.l2];
|
|
64
|
+
if (!entry) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
68
|
+
delete dataHolder[key.l1][key.l2];
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return entry;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
'lock': (key, ttl) => {
|
|
75
|
+
if (!dataHolder[key.l1]) {
|
|
76
|
+
dataHolder[key.l1] = {};
|
|
77
|
+
}
|
|
78
|
+
const existing = dataHolder[key.l1][key.l2];
|
|
79
|
+
if (existing && existing.expiresAt && Date.now() <= existing.expiresAt) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
dataHolder[key.l1][key.l2] = {
|
|
83
|
+
'status': 'in_flight',
|
|
84
|
+
'lockedAt': Date.now(),
|
|
85
|
+
'expiresAt': Date.now() + ttl
|
|
86
|
+
};
|
|
87
|
+
return true;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
'complete': (key, response, ttl) => {
|
|
91
|
+
if (!dataHolder[key.l1]) {
|
|
92
|
+
dataHolder[key.l1] = {};
|
|
93
|
+
}
|
|
94
|
+
dataHolder[key.l1][key.l2] = {
|
|
95
|
+
'status': 'completed',
|
|
96
|
+
'response': response,
|
|
97
|
+
'completedAt': Date.now(),
|
|
98
|
+
'expiresAt': Date.now() + ttl
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
'unlock': (key) => {
|
|
103
|
+
if (dataHolder[key.l1] && dataHolder[key.l1][key.l2]) {
|
|
104
|
+
delete dataHolder[key.l1][key.l2];
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
'shutdown': () => {
|
|
109
|
+
if (cleanupInterval) {
|
|
110
|
+
clearInterval(cleanupInterval);
|
|
111
|
+
cleanupInterval = null;
|
|
112
|
+
}
|
|
113
|
+
dataHolder = {};
|
|
114
|
+
if (model.log) {
|
|
115
|
+
model.log.info('Idempotency memory model shutdown complete');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (typeof process !== 'undefined') {
|
|
121
|
+
const shutdownHandler = () => {
|
|
122
|
+
model.shutdown();
|
|
123
|
+
};
|
|
124
|
+
process.once('SIGTERM', shutdownHandler);
|
|
125
|
+
process.once('SIGINT', shutdownHandler);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = model;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const core = require('soajs.core.modules');
|
|
12
|
+
const Mongo = core.mongo;
|
|
13
|
+
|
|
14
|
+
const idempotency_store = 'idempotency_store';
|
|
15
|
+
|
|
16
|
+
let model = {
|
|
17
|
+
'mongo': null,
|
|
18
|
+
'log': null,
|
|
19
|
+
|
|
20
|
+
'init': (log, dbConfiguration) => {
|
|
21
|
+
model.log = log;
|
|
22
|
+
if (!model.mongo) {
|
|
23
|
+
model.mongo = new Mongo(dbConfiguration);
|
|
24
|
+
model.mongo.createIndex(idempotency_store, { 'l1': 1, 'l2': 1 }, { unique: true }, (err, index) => {
|
|
25
|
+
log.debug("Idempotency index: " + index + " created with error: " + err);
|
|
26
|
+
});
|
|
27
|
+
model.mongo.createIndex(idempotency_store, { 'expiresAt': 1 }, { expireAfterSeconds: 0 }, (err, index) => {
|
|
28
|
+
log.debug("Idempotency TTL index: " + index + " created with error: " + err);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
'get': async (key) => {
|
|
34
|
+
let condition = {
|
|
35
|
+
'l1': key.l1,
|
|
36
|
+
'l2': key.l2
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const entry = await model.mongo.findOne(idempotency_store, condition);
|
|
40
|
+
if (!entry) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt.getTime()) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return entry;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
model.log.error('Idempotency get:', e.message);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
'lock': async (key, ttl) => {
|
|
54
|
+
let condition = {
|
|
55
|
+
'l1': key.l1,
|
|
56
|
+
'l2': key.l2
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const existing = await model.mongo.findOne(idempotency_store, condition);
|
|
60
|
+
if (existing && existing.expiresAt && Date.now() <= existing.expiresAt.getTime()) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let doc = {
|
|
65
|
+
'l1': key.l1,
|
|
66
|
+
'l2': key.l2,
|
|
67
|
+
'status': 'in_flight',
|
|
68
|
+
'lockedAt': new Date(),
|
|
69
|
+
'expiresAt': new Date(Date.now() + ttl)
|
|
70
|
+
};
|
|
71
|
+
let options = { 'upsert': true };
|
|
72
|
+
let s = { '$set': doc };
|
|
73
|
+
await model.mongo.updateOne(idempotency_store, condition, s, options);
|
|
74
|
+
return true;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (e.code === 11000) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
model.log.error('Idempotency lock:', e.message);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
'complete': async (key, response, ttl) => {
|
|
85
|
+
let condition = {
|
|
86
|
+
'l1': key.l1,
|
|
87
|
+
'l2': key.l2
|
|
88
|
+
};
|
|
89
|
+
let doc = {
|
|
90
|
+
'status': 'completed',
|
|
91
|
+
'response': response,
|
|
92
|
+
'completedAt': new Date(),
|
|
93
|
+
'expiresAt': new Date(Date.now() + ttl)
|
|
94
|
+
};
|
|
95
|
+
let s = { '$set': doc };
|
|
96
|
+
try {
|
|
97
|
+
await model.mongo.updateOne(idempotency_store, condition, s, {});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
model.log.error('Idempotency complete:', e.message);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
'unlock': async (key) => {
|
|
104
|
+
let condition = {
|
|
105
|
+
'l1': key.l1,
|
|
106
|
+
'l2': key.l2
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
await model.mongo.deleteOne(idempotency_store, condition);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
model.log.error('Idempotency unlock:', e.message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
module.exports = model;
|
package/mw/mt/lib.js
CHANGED
|
@@ -13,7 +13,7 @@ let _ = require("lodash");
|
|
|
13
13
|
|
|
14
14
|
module.exports = {
|
|
15
15
|
"mergeACLArray": function (arrayACL, cb) {
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
//if only one return it
|
|
18
18
|
if (arrayACL.length === 1) {
|
|
19
19
|
return cb(null, arrayACL[0]);
|
|
@@ -30,30 +30,30 @@ module.exports = {
|
|
|
30
30
|
function () {
|
|
31
31
|
return cb(null, acl);
|
|
32
32
|
});
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
function mergePackageAcl(oneAcl, cb) {
|
|
35
35
|
//first object
|
|
36
36
|
if (!acl.acl) {
|
|
37
|
-
acl.acl = oneAcl.acl;
|
|
37
|
+
acl.acl = _.cloneDeep(oneAcl.acl);
|
|
38
38
|
return cb();
|
|
39
39
|
}
|
|
40
40
|
async.eachOfSeries(oneAcl.acl, function (service, serviceName, serviceCall) {
|
|
41
41
|
if (!acl.acl[serviceName]) {
|
|
42
|
-
acl.acl[serviceName] = service;
|
|
42
|
+
acl.acl[serviceName] = _.cloneDeep(service);
|
|
43
43
|
return serviceCall();
|
|
44
44
|
}
|
|
45
45
|
async.eachOfSeries(service, function (version, versionValue, versionCall) {
|
|
46
46
|
if (!acl.acl[serviceName][versionValue]) {
|
|
47
|
-
acl.acl[serviceName][versionValue] = version;
|
|
47
|
+
acl.acl[serviceName][versionValue] = _.cloneDeep(version);
|
|
48
48
|
return serviceCall();
|
|
49
49
|
}
|
|
50
50
|
if (version.apisPermission === "restricted") {
|
|
51
51
|
if (acl.acl[serviceName] && acl.acl[serviceName][versionValue] && acl.acl[serviceName][versionValue].apisPermission && acl.acl[serviceName][versionValue].apisPermission === "restricted") {
|
|
52
52
|
//only restricted allowed to merge
|
|
53
|
-
acl.acl[serviceName][versionValue] = _.merge(acl.acl[serviceName][versionValue], version);
|
|
53
|
+
acl.acl[serviceName][versionValue] = _.merge(acl.acl[serviceName][versionValue], _.cloneDeep(version));
|
|
54
54
|
return versionCall();
|
|
55
55
|
} else {
|
|
56
|
-
acl.acl[serviceName][versionValue] = version;
|
|
56
|
+
acl.acl[serviceName][versionValue] = _.cloneDeep(version);
|
|
57
57
|
return versionCall();
|
|
58
58
|
}
|
|
59
59
|
} else {
|
|
@@ -62,42 +62,42 @@ module.exports = {
|
|
|
62
62
|
return versionCall();
|
|
63
63
|
} else {
|
|
64
64
|
//only non restricted allowed to merge
|
|
65
|
-
acl.acl[serviceName][versionValue] = _.merge(acl.acl[serviceName][versionValue], version);
|
|
65
|
+
acl.acl[serviceName][versionValue] = _.merge(acl.acl[serviceName][versionValue], _.cloneDeep(version));
|
|
66
66
|
return versionCall();
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}, serviceCall);
|
|
70
70
|
}, cb);
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
function mergePackageAclAllEnv(oneAcl, cb) {
|
|
74
74
|
//first object
|
|
75
75
|
if (!acl.acl_all_env) {
|
|
76
|
-
acl.acl_all_env = oneAcl.acl_all_env;
|
|
76
|
+
acl.acl_all_env = _.cloneDeep(oneAcl.acl_all_env);
|
|
77
77
|
return cb();
|
|
78
78
|
}
|
|
79
79
|
async.eachOf(oneAcl.acl_all_env, function (environment, envName, envCall) {
|
|
80
80
|
if (!acl.acl_all_env[envName]) {
|
|
81
|
-
acl.acl_all_env[envName] = environment;
|
|
81
|
+
acl.acl_all_env[envName] = _.cloneDeep(environment);
|
|
82
82
|
return envCall();
|
|
83
83
|
}
|
|
84
84
|
async.eachOf(environment, function (service, serviceName, serviceCall) {
|
|
85
85
|
if (!acl.acl_all_env[envName][serviceName]) {
|
|
86
|
-
acl.acl_all_env[envName][serviceName] = service;
|
|
86
|
+
acl.acl_all_env[envName][serviceName] = _.cloneDeep(service);
|
|
87
87
|
return serviceCall();
|
|
88
88
|
}
|
|
89
89
|
async.eachOf(service, function (version, versionValue, versionCall) {
|
|
90
90
|
if (!acl.acl_all_env[envName][serviceName][versionValue]) {
|
|
91
|
-
acl.acl_all_env[envName][serviceName][versionValue] = version;
|
|
91
|
+
acl.acl_all_env[envName][serviceName][versionValue] = _.cloneDeep(version);
|
|
92
92
|
return serviceCall();
|
|
93
93
|
}
|
|
94
94
|
if (version.apisPermission === "restricted") {
|
|
95
95
|
if (acl.acl_all_env[envName][serviceName][versionValue].apisPermission && acl.acl_all_env[envName][serviceName][versionValue].apisPermission === "restricted") {
|
|
96
96
|
//only restricted allowed to merge
|
|
97
|
-
acl.acl_all_env[envName][serviceName][versionValue] = _.merge(acl.acl_all_env[envName][serviceName][versionValue], version);
|
|
97
|
+
acl.acl_all_env[envName][serviceName][versionValue] = _.merge(acl.acl_all_env[envName][serviceName][versionValue], _.cloneDeep(version));
|
|
98
98
|
return versionCall();
|
|
99
99
|
} else {
|
|
100
|
-
acl.acl_all_env[envName][serviceName][versionValue] = version;
|
|
100
|
+
acl.acl_all_env[envName][serviceName][versionValue] = _.cloneDeep(version);
|
|
101
101
|
return versionCall();
|
|
102
102
|
}
|
|
103
103
|
} else {
|
|
@@ -106,7 +106,7 @@ module.exports = {
|
|
|
106
106
|
return versionCall();
|
|
107
107
|
} else {
|
|
108
108
|
//only non restricted allowed to merge
|
|
109
|
-
acl.acl_all_env[envName][serviceName][versionValue] = _.merge(acl.acl_all_env[envName][serviceName][versionValue], version);
|
|
109
|
+
acl.acl_all_env[envName][serviceName][versionValue] = _.merge(acl.acl_all_env[envName][serviceName][versionValue], _.cloneDeep(version));
|
|
110
110
|
return versionCall();
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -115,4 +115,4 @@ module.exports = {
|
|
|
115
115
|
}, cb);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
};
|
|
118
|
+
};
|
|
@@ -31,7 +31,7 @@ module.exports = (configuration) => {
|
|
|
31
31
|
}
|
|
32
32
|
if (!ACL && req.soajs.controller.serviceParams.packObj.acl) {
|
|
33
33
|
req.soajs.log.debug("soajsRoute keyACL: Found Default ACL at Package level, setting default ACL configuration.");
|
|
34
|
-
ACL = req.soajs.controller.serviceParams.packObj.acl;
|
|
34
|
+
ACL = coreLibs.utils.cloneObj(req.soajs.controller.serviceParams.packObj.acl);
|
|
35
35
|
}
|
|
36
36
|
let finalACL = null;
|
|
37
37
|
if (ACL) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soajs.controller",
|
|
3
3
|
"description": "soajs multi tenant API gateway",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "soajs team",
|
|
7
7
|
"email": "team@soajs.org"
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"merge": "2.1.1",
|
|
28
28
|
"netmask": "2.0.2",
|
|
29
29
|
"oauth2-server": "2.4.1",
|
|
30
|
-
"path-to-regexp": "8.
|
|
30
|
+
"path-to-regexp": "8.4.0",
|
|
31
31
|
"soajs.core.libs": "1.2.2",
|
|
32
|
-
"soajs.core.modules": "5.2.
|
|
32
|
+
"soajs.core.modules": "5.2.19",
|
|
33
33
|
"soajs.urac.driver": "3.2.15",
|
|
34
34
|
"useragent": "2.3.0"
|
|
35
35
|
},
|
package/server/controller.js
CHANGED
|
@@ -37,6 +37,8 @@ const keyACL_mw = require("../mw/keyACL/index");
|
|
|
37
37
|
const gotoService_mw = require("../mw/gotoService/index");
|
|
38
38
|
const mt_mw = require("../mw/mt/index");
|
|
39
39
|
const traffic_mw = require("../mw/traffic/index");
|
|
40
|
+
const idempotency_mw = require("../mw/idempotency/index");
|
|
41
|
+
const cache_mw = require("../mw/cache/index");
|
|
40
42
|
const lastSeen_mw = require("../mw/lastSeen/index");
|
|
41
43
|
const ip2ban_mw = require("../mw/ip2ban/index");
|
|
42
44
|
|
|
@@ -175,11 +177,23 @@ Controller.prototype.init = function (callback) {
|
|
|
175
177
|
}));
|
|
176
178
|
log.info("SOAJS MT middleware initialization done.");
|
|
177
179
|
|
|
180
|
+
app.use(idempotency_mw({
|
|
181
|
+
"log": log,
|
|
182
|
+
"gatewayDB": registry.coreDB.gateway || registry.coreDB.provision
|
|
183
|
+
}));
|
|
184
|
+
log.info("Idempotency middleware initialization done.");
|
|
185
|
+
|
|
178
186
|
app.use(traffic_mw({
|
|
179
187
|
"log": log,
|
|
180
188
|
"gatewayDB": registry.coreDB.gateway || registry.coreDB.provision
|
|
181
189
|
}));
|
|
182
190
|
|
|
191
|
+
app.use(cache_mw({
|
|
192
|
+
"log": log,
|
|
193
|
+
"gatewayDB": registry.coreDB.gateway || registry.coreDB.provision
|
|
194
|
+
}));
|
|
195
|
+
log.info("Cache middleware initialization done.");
|
|
196
|
+
|
|
183
197
|
app.use(lastSeen_mw());
|
|
184
198
|
|
|
185
199
|
app.use((req, res, next) => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
- i have 2 users with access to the same tenant
|
|
2
|
+
- under this tenant i have 2 groups as follow:
|
|
3
|
+
- owner with the following packages [GRPCH_DEFAU, GRPCH_OWNER]
|
|
4
|
+
- member with the following packages [GRPCH_DEFAU, GRPCH_AMMBR, GRPCH_EGDTL, GRPCH_SMDIA, GRPCH_SMSSG, GRPCH_VMMBR]
|
|
5
|
+
- i have microservice connectonboarding with an api DELETE /group/chats/users/:userId and this api is only under package GRPCH_OWNER
|
|
6
|
+
|
|
7
|
+
i have the following usecase:
|
|
8
|
+
- load the provision by hitting the maintenance route /loadProvision
|
|
9
|
+
- user1 with member group login first and hit the acl api /soajs/acl, then we check the response and we see that he has the right packages and api DELETE /group/chats/users/:userId is not part of finalACL which is correct
|
|
10
|
+
- user2 with owner group login and hit the acl api /soajs/acl, then we check the response and we see that he has the right packages and api DELETE /group/chats/users/:userId is part of finalACL which is correct
|
|
11
|
+
- user1 with member group hit the acl api /soajs/acl again, then we check the response and we see that he has the right packages but api DELETE /group/chats/users/:userId is now part of finalACL which is not correct
|
package/test/unit/index.js
CHANGED
|
@@ -34,6 +34,10 @@ describe("Starting Gateway Unit test", () => {
|
|
|
34
34
|
require("./mw/response/index.js");
|
|
35
35
|
require("./mw/soajs/index.js");
|
|
36
36
|
require("./mw/traffic/index.js");
|
|
37
|
+
require("./mw/idempotency/index.js");
|
|
38
|
+
require("./mw/idempotency/memory.js");
|
|
39
|
+
require("./mw/cache/index.js");
|
|
40
|
+
require("./mw/cache/memory.js");
|
|
37
41
|
require("./mw/lastSeen/index.js");
|
|
38
42
|
|
|
39
43
|
require("./utilities/utils.js");
|