soajs.controller 4.3.13 → 4.3.14

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.
@@ -292,11 +292,24 @@ Tracks user activity by notifying a service (typically URAC) of user access.
292
292
  "serviceName": "urac", // Service to notify
293
293
  "serviceVersion": "3", // Service version
294
294
  "api": "/user/last/seen", // API endpoint
295
- "network": "internal" // Optional network identifier
295
+ "network": "internal", // Optional network identifier
296
+ "include": { // Optional whitelist, omit for all requests
297
+ "connectspaces": true
298
+ },
299
+ "targets": [ // Optional extra APIs, same event
300
+ {
301
+ "serviceName": "authenticator",
302
+ "serviceVersion": "1",
303
+ "api": "/my/device/network",
304
+ "method": "put"
305
+ }
306
+ ]
296
307
  }
297
308
  }
298
309
  ```
299
310
 
311
+ See `docs/middleware/lastSeen.md` for the `include` matching rules and the target fields.
312
+
300
313
  | Field | Type | Default | Description |
301
314
  |-------|------|---------|-------------|
302
315
  | `active` | boolean | `false` | Enable/disable activity tracking |
@@ -69,7 +69,18 @@ Located in `registry.custom.gateway.value.lastSeen`:
69
69
  "serviceName": "urac", // Target service
70
70
  "serviceVersion": "3", // Service version
71
71
  "api": "/user/last/seen", // API endpoint
72
- "network": "internal" // Optional network identifier
72
+ "network": "internal", // Optional network identifier
73
+ "include": { // Optional whitelist, omit to trigger on everything
74
+ "connectspaces": true
75
+ },
76
+ "targets": [ // Optional extra APIs notified on the same event
77
+ {
78
+ "serviceName": "authenticator",
79
+ "serviceVersion": "1",
80
+ "api": "/my/device/network",
81
+ "method": "put"
82
+ }
83
+ ]
73
84
  }
74
85
  }
75
86
  ```
@@ -81,6 +92,40 @@ Located in `registry.custom.gateway.value.lastSeen`:
81
92
  | `serviceVersion` | string | `"3"` | Service version |
82
93
  | `api` | string | `"/user/last/seen"` | API endpoint |
83
94
  | `network` | string | - | Network identifier in request body |
95
+ | `include` | object | - | Whitelist of services and APIs, omit to trigger on every request |
96
+ | `targets` | array | - | Extra APIs notified on the same event |
97
+
98
+ ### include
99
+
100
+ Decides whether a request counts as activity. Omitting it triggers on every request.
101
+
102
+ | Form | Meaning |
103
+ |------|---------|
104
+ | service missing from `include` | never triggers |
105
+ | `"service": true` | every API, every method |
106
+ | `"service": {"apis": {"/path": true}}` | that API, every method |
107
+ | `"service": {"apis": {"/path": ["get","put"]}}` | that API, listed methods only |
108
+ | `"apis": {"*": ...}` | any API, checked before the exact paths |
109
+
110
+ API keys accept the service relative path (`/active`) or the full path
111
+ (`/connectspaces/active`), and support path params (`/user/:id`).
112
+
113
+ ### targets
114
+
115
+ Extra APIs notified whenever the main one is. They are not filtered on their own, the
116
+ `include` above has already decided that the request counts as activity, so a target is
117
+ notified on exactly the same set of requests.
118
+
119
+ | Field | Type | Default | Description |
120
+ |-------|------|---------|-------------|
121
+ | `serviceName` | string | required | Service to notify, must be in the registry |
122
+ | `api` | string | required | API endpoint |
123
+ | `serviceVersion` | string | - | Service version, resolved by awareness when absent |
124
+ | `method` | string | `"post"` | HTTP method |
125
+
126
+ Each target receives the same body and the same `soajsinjectobj` header as the main
127
+ notification. A target that is down, unknown to the registry, or missing `serviceName`
128
+ or `api` is skipped without affecting the others.
84
129
 
85
130
  ## Request to URAC
86
131
 
@@ -143,6 +143,56 @@ function isMethodMatch(methodConfig, method) {
143
143
  return false;
144
144
  }
145
145
 
146
+ /**
147
+ * Fire and forget the notification at one target.
148
+ *
149
+ * NOTE: the request is not awaited, a target that is down or slow must not affect
150
+ * the original request, which has already been handed to the next middleware.
151
+ *
152
+ * @param req
153
+ * @param lastSeen the custom registry block, carries the shared body
154
+ * @param item {name, version, api, method}
155
+ */
156
+ function notifyTarget(req, lastSeen, item) {
157
+ //NOTE: getHost resolves against the registry, an unknown service would throw on
158
+ // the port lookup below and take the remaining targets down with it
159
+ if (!req.soajs.registry || !req.soajs.registry.services ||
160
+ !req.soajs.registry.services[item.name] || !req.soajs.registry.services[item.name].port) {
161
+ req.soajs.log.debug("lastSeen failed, service not in registry [" + item.name + "@" + item.version + " " + item.api + "]");
162
+ return;
163
+ }
164
+ req.soajs.awareness.getHost(item.name, item.version, (host) => {
165
+ if (!host) {
166
+ req.soajs.log.debug("lastSeen failed with no host for [" + item.name + "@" + item.version + " " + item.api + "]");
167
+ return;
168
+ }
169
+ item.host = host;
170
+ item.port = req.soajs.registry.services[item.name].port;
171
+
172
+ let options = {
173
+ "uri": "http://" + item.host + ":" + item.port + item.api,
174
+ "json": true,
175
+ "method": item.method || "POST"
176
+ };
177
+ if (lastSeen.network) {
178
+ options.body = { "network": lastSeen.network };
179
+ }
180
+ if (req.headers && req.headers.soajsinjectobj) {
181
+ options.headers = { "soajsinjectobj": req.headers.soajsinjectobj };
182
+ }
183
+ httpRequest(options)
184
+ .then(() => { })
185
+ .catch(({ error, body }) => {
186
+ if (error) {
187
+ req.soajs.log.debug("lastSeen failed for [" + item.name + "@" + item.version + " " + item.api + "]");
188
+ req.soajs.log.debug("lastSeen " + error.message);
189
+ } else {
190
+ req.soajs.log.debug("lastSeen ", body);
191
+ }
192
+ });
193
+ });
194
+ }
195
+
146
196
  /**
147
197
  *
148
198
  * @param configuration
@@ -180,39 +230,29 @@ module.exports = () => {
180
230
  if (uracObj) {
181
231
  userId = uracObj._id;
182
232
  }
183
- if (userId) {
184
- req.soajs.awareness.getHost(item.name, item.version, (host) => {
185
- if (host) {
186
- item.host = host;
187
- item.port = req.soajs.registry.services[item.name].port;
188
-
189
- let options = {
190
- "uri": "http://" + item.host + ":" + item.port + item.api,
191
- "json": true,
192
- "method": "POST"
193
- };
194
- if (lastSeen.network) {
195
- options.body = { "network": lastSeen.network };
196
- }
197
- if (req.headers && req.headers.soajsinjectobj) {
198
- options.headers = { "soajsinjectobj": req.headers.soajsinjectobj };
199
- }
200
- httpRequest(options)
201
- .then(() => { })
202
- .catch(({ error, body }) => {
203
- if (error) {
204
- req.soajs.log.debug("lastSeen failed for [" + item.name + "@" + item.version + " " + item.api + "]");
205
- req.soajs.log.debug("lastSeen " + error.message);
206
- } else {
207
- req.soajs.log.debug("lastSeen ", body);
208
- }
209
- });
210
- } else {
211
- req.soajs.log.debug("lastSeen failed with no host for [" + item.name + "@" + item.version + " " + item.api + "]");
233
+ if (!userId) {
234
+ req.soajs.log.debug("lastSeen failed with no userId for [" + item.name + "@" + item.version + " " + item.api + "]");
235
+ return;
236
+ }
237
+
238
+ notifyTarget(req, lastSeen, item);
239
+
240
+ //NOTE: additional targets are notified on the same event, they are not
241
+ // filtered on their own, the include above already decided that this
242
+ // request counts as activity
243
+ if (Array.isArray(lastSeen.targets)) {
244
+ lastSeen.targets.forEach((target) => {
245
+ if (!target || !target.serviceName || !target.api) {
246
+ req.soajs.log.debug("lastSeen target skipped, serviceName and api are required");
247
+ return;
212
248
  }
249
+ notifyTarget(req, lastSeen, {
250
+ "name": target.serviceName,
251
+ "version": target.serviceVersion || null,
252
+ "api": target.api,
253
+ "method": target.method
254
+ });
213
255
  });
214
- } else {
215
- req.soajs.log.debug("lastSeen failed with no userId for [" + item.name + "@" + item.version + " " + item.api + "]");
216
256
  }
217
257
  }
218
258
  }
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.13",
4
+ "version": "4.3.14",
5
5
  "author": {
6
6
  "name": "soajs team",
7
7
  "email": "team@soajs.org"
@@ -1009,4 +1009,172 @@ describe("Unit test for: mw - lastSeen", () => {
1009
1009
 
1010
1010
  });
1011
1011
 
1012
+ describe("extra targets tests", () => {
1013
+
1014
+ let buildReq = (lastSeen) => {
1015
+ return {
1016
+ method: "GET",
1017
+ getClientIP: () => {
1018
+ return "127.0.0.1";
1019
+ },
1020
+ soajs: {
1021
+ log: {
1022
+ debug: () => {
1023
+ },
1024
+ error: () => {
1025
+ }
1026
+ },
1027
+ uracDriver: {
1028
+ getProfile: () => {
1029
+ return { _id: "1111111111" };
1030
+ }
1031
+ },
1032
+ awareness: {
1033
+ getHost: (name, version, cb) => {
1034
+ return cb(name + ".fake");
1035
+ }
1036
+ },
1037
+ controller: {
1038
+ serviceParams: {
1039
+ name: "connectspaces",
1040
+ path: "/active",
1041
+ parsedUrl: { pathname: "/connectspaces/active" }
1042
+ }
1043
+ },
1044
+ registry: {
1045
+ services: {
1046
+ urac: { port: 4001 },
1047
+ authenticator: { port: 4002 }
1048
+ },
1049
+ custom: {
1050
+ gateway: {
1051
+ value: { lastSeen: lastSeen }
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+ };
1057
+ };
1058
+
1059
+ it("notifies the extra target alongside urac, honouring its method", (done) => {
1060
+ let req = buildReq({
1061
+ active: true,
1062
+ network: "YAYA",
1063
+ targets: [{
1064
+ serviceName: "authenticator",
1065
+ serviceVersion: "1",
1066
+ api: "/my/device/network",
1067
+ method: "put"
1068
+ }]
1069
+ });
1070
+ const uracScope = nock('http://urac.fake:4001')
1071
+ .post('/user/last/seen', "{\"network\":\"YAYA\"}")
1072
+ .reply(200, { result: true });
1073
+ const authScope = nock('http://authenticator.fake:4002')
1074
+ .put('/my/device/network', "{\"network\":\"YAYA\"}")
1075
+ .reply(200, { result: true });
1076
+
1077
+ let functionMw = mw({});
1078
+ functionMw(req, {}, (error) => {
1079
+ assert.ifError(error);
1080
+ });
1081
+ setTimeout(() => {
1082
+ assert.strictEqual(uracScope.isDone(), true, "urac was not notified");
1083
+ assert.strictEqual(authScope.isDone(), true, "the extra target was not notified");
1084
+ done();
1085
+ }, 100);
1086
+ });
1087
+
1088
+ it("defaults the extra target to post when no method is set", (done) => {
1089
+ let req = buildReq({
1090
+ active: true,
1091
+ targets: [{
1092
+ serviceName: "authenticator",
1093
+ serviceVersion: "1",
1094
+ api: "/my/device/network"
1095
+ }]
1096
+ });
1097
+ nock('http://urac.fake:4001').post('/user/last/seen').reply(200, { result: true });
1098
+ const authScope = nock('http://authenticator.fake:4002')
1099
+ .post('/my/device/network')
1100
+ .reply(200, { result: true });
1101
+
1102
+ let functionMw = mw({});
1103
+ functionMw(req, {}, () => {
1104
+ });
1105
+ setTimeout(() => {
1106
+ assert.strictEqual(authScope.isDone(), true, "the extra target was not posted to");
1107
+ done();
1108
+ }, 100);
1109
+ });
1110
+
1111
+ it("still notifies urac when a target names a service that is not in the registry", (done) => {
1112
+ let req = buildReq({
1113
+ active: true,
1114
+ targets: [{
1115
+ serviceName: "doesnotexist",
1116
+ serviceVersion: "1",
1117
+ api: "/nope"
1118
+ }]
1119
+ });
1120
+ const uracScope = nock('http://urac.fake:4001')
1121
+ .post('/user/last/seen')
1122
+ .reply(200, { result: true });
1123
+
1124
+ let functionMw = mw({});
1125
+ assert.doesNotThrow(() => {
1126
+ functionMw(req, {}, () => {
1127
+ });
1128
+ });
1129
+ setTimeout(() => {
1130
+ assert.strictEqual(uracScope.isDone(), true, "urac was not notified");
1131
+ done();
1132
+ }, 100);
1133
+ });
1134
+
1135
+ it("skips a malformed target and still notifies urac", (done) => {
1136
+ let req = buildReq({
1137
+ active: true,
1138
+ targets: [{ serviceVersion: "1" }, null]
1139
+ });
1140
+ const uracScope = nock('http://urac.fake:4001')
1141
+ .post('/user/last/seen')
1142
+ .reply(200, { result: true });
1143
+
1144
+ let functionMw = mw({});
1145
+ functionMw(req, {}, () => {
1146
+ });
1147
+ setTimeout(() => {
1148
+ assert.strictEqual(uracScope.isDone(), true, "urac was not notified");
1149
+ done();
1150
+ }, 100);
1151
+ });
1152
+
1153
+ it("does not notify any target when the include filter excludes the request", (done) => {
1154
+ let req = buildReq({
1155
+ active: true,
1156
+ include: { "someotherservice": true },
1157
+ targets: [{
1158
+ serviceName: "authenticator",
1159
+ serviceVersion: "1",
1160
+ api: "/my/device/network",
1161
+ method: "put"
1162
+ }]
1163
+ });
1164
+ const uracScope = nock('http://urac.fake:4001').post('/user/last/seen').reply(200, {});
1165
+ const authScope = nock('http://authenticator.fake:4002').put('/my/device/network').reply(200, {});
1166
+
1167
+ let functionMw = mw({});
1168
+ functionMw(req, {}, () => {
1169
+ });
1170
+ setTimeout(() => {
1171
+ assert.strictEqual(uracScope.isDone(), false, "urac should have been skipped");
1172
+ assert.strictEqual(authScope.isDone(), false, "the extra target should have been skipped");
1173
+ nock.cleanAll();
1174
+ done();
1175
+ }, 100);
1176
+ });
1177
+
1178
+ });
1179
+
1012
1180
  });