soajs.controller 4.3.13 → 4.3.15
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/docs/configuration-custom-registry.md +98 -1
- package/docs/middleware/lastSeen.md +46 -1
- package/docs/middleware/mt.md +1 -0
- package/lib/membership.js +74 -0
- package/mw/lastSeen/index.js +71 -31
- package/mw/mt/index.js +12 -0
- package/mw/mt/urac.js +7 -0
- package/package.json +1 -1
- package/test/unit/index.js +1 -0
- package/test/unit/lib/membership.js +146 -0
- package/test/unit/mw/lastSeen/index.js +168 -0
|
@@ -27,6 +27,9 @@ registry.custom
|
|
|
27
27
|
│ │ └── [service] # Service-specific settings
|
|
28
28
|
│ ├── maintenanceMode # Gateway maintenance mode
|
|
29
29
|
│ ├── lastSeen # User activity tracking
|
|
30
|
+
│ ├── membership # Membership on the urac object, by product
|
|
31
|
+
│ │ ├── active # Whether membership is resolved at all
|
|
32
|
+
│ │ └── [main product] # Its sub products
|
|
30
33
|
│ ├── oauth
|
|
31
34
|
│ │ └── deviceIdCheck # Turn the deviceId check off
|
|
32
35
|
│ └── gotoService
|
|
@@ -292,11 +295,24 @@ Tracks user activity by notifying a service (typically URAC) of user access.
|
|
|
292
295
|
"serviceName": "urac", // Service to notify
|
|
293
296
|
"serviceVersion": "3", // Service version
|
|
294
297
|
"api": "/user/last/seen", // API endpoint
|
|
295
|
-
"network": "internal"
|
|
298
|
+
"network": "internal", // Optional network identifier
|
|
299
|
+
"include": { // Optional whitelist, omit for all requests
|
|
300
|
+
"connectspaces": true
|
|
301
|
+
},
|
|
302
|
+
"targets": [ // Optional extra APIs, same event
|
|
303
|
+
{
|
|
304
|
+
"serviceName": "authenticator",
|
|
305
|
+
"serviceVersion": "1",
|
|
306
|
+
"api": "/my/device/network",
|
|
307
|
+
"method": "put"
|
|
308
|
+
}
|
|
309
|
+
]
|
|
296
310
|
}
|
|
297
311
|
}
|
|
298
312
|
```
|
|
299
313
|
|
|
314
|
+
See `docs/middleware/lastSeen.md` for the `include` matching rules and the target fields.
|
|
315
|
+
|
|
300
316
|
| Field | Type | Default | Description |
|
|
301
317
|
|-------|------|---------|-------------|
|
|
302
318
|
| `active` | boolean | `false` | Enable/disable activity tracking |
|
|
@@ -481,6 +497,86 @@ is read by the oauth middleware itself.
|
|
|
481
497
|
|
|
482
498
|
---
|
|
483
499
|
|
|
500
|
+
## 14. Membership
|
|
501
|
+
|
|
502
|
+
**Path:** `registry.custom.gateway.value.membership`
|
|
503
|
+
|
|
504
|
+
**Middleware:** `mw/mt/index.js`, resolved by `lib/membership.js`
|
|
505
|
+
|
|
506
|
+
Puts the caller's membership on the urac object of every call, so a service can read it
|
|
507
|
+
without a lookup.
|
|
508
|
+
|
|
509
|
+
A membership is held per main product on the user record in the urac `users` collection,
|
|
510
|
+
and only its name is stored. What the name grants is resolved from the catalog, it is
|
|
511
|
+
never copied onto the user:
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
{
|
|
515
|
+
"memberships": [
|
|
516
|
+
{ "product": "AVAPP", "membership": "whale" }
|
|
517
|
+
]
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
The gateway already resolves both halves of the match on every call. The access token
|
|
522
|
+
gives the user record, which carries `memberships`, and the API key gives the product the
|
|
523
|
+
call is made under. Resolution is the match between them: if the product of the request
|
|
524
|
+
is a configured main product or one of its sub products, and the user holds an entry for
|
|
525
|
+
that main product, the membership name is injected.
|
|
526
|
+
|
|
527
|
+
```javascript
|
|
528
|
+
{
|
|
529
|
+
"membership": {
|
|
530
|
+
"active": true, // Resolve membership at all
|
|
531
|
+
"AVAPP": ["CLUBS", "CORPS", "SECRT", "GRPCH"] // Main product and its sub products
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
| Field | Type | Default | Description |
|
|
537
|
+
|-------|------|---------|-------------|
|
|
538
|
+
| `active` | boolean | `false` | Whether the gateway resolves membership at all |
|
|
539
|
+
| `[main product]` | string[] | - | A product a membership is held in, valued by the sub products that resolve to it |
|
|
540
|
+
|
|
541
|
+
Every key other than `active` is read as a main product, so more than one can be
|
|
542
|
+
configured and they stay independent. A main product always resolves to itself, listing
|
|
543
|
+
sub products is optional.
|
|
544
|
+
|
|
545
|
+
### What the service sees
|
|
546
|
+
|
|
547
|
+
```javascript
|
|
548
|
+
req.soajs.urac.membership // "whale"
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Only the resolved name travels. The raw `memberships` array is never injected, so a
|
|
552
|
+
service cannot read a membership held for a product the request was not made under.
|
|
553
|
+
|
|
554
|
+
### It fails closed
|
|
555
|
+
|
|
556
|
+
Nothing is injected, and no service sees a membership, when any of these hold:
|
|
557
|
+
|
|
558
|
+
| | |
|
|
559
|
+
|---|---|
|
|
560
|
+
| the block is absent, or `active` is not `true` | membership is off |
|
|
561
|
+
| no main product is configured | nothing is resolvable, equivalent to off |
|
|
562
|
+
| the request product is neither a main product nor a sub product | out of scope |
|
|
563
|
+
| the user holds no entry for the resolved main product | no membership |
|
|
564
|
+
| the service does not request urac (`urac: false`) | there is no urac object to put it on |
|
|
565
|
+
|
|
566
|
+
Product codes are matched exactly. They are uppercase by convention, and normalizing
|
|
567
|
+
here would hide a miscased entry in the registry rather than surface it.
|
|
568
|
+
|
|
569
|
+
### Freshness
|
|
570
|
+
|
|
571
|
+
The user record behind this comes from the access token when
|
|
572
|
+
`serviceConfig.oauth.getUserFromToken` is on, when the login mode is `oauth`, or when
|
|
573
|
+
roaming. In those cases `memberships` is a snapshot taken at login, and a membership
|
|
574
|
+
changed afterwards is not seen until a new token is issued. Refreshing does not help,
|
|
575
|
+
the refresh grant copies the stored user forward unchanged. Otherwise the record is
|
|
576
|
+
loaded from urac per request and the membership is current.
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
484
580
|
## Complete Configuration Example
|
|
485
581
|
|
|
486
582
|
```javascript
|
|
@@ -602,6 +698,7 @@ Custom registry configurations are processed in this order:
|
|
|
602
698
|
| 5 | gotoService/redirectToService | `gotoService.monitor` |
|
|
603
699
|
| 6 | mt (security checks) | `mt.whitelist`, `oauth.value.pinWrapper`, `oauth.value.pinWhitelist` |
|
|
604
700
|
| 6a | oauth (called from mt) | `oauth.deviceIdCheck` |
|
|
701
|
+
| 6b | mt (inject object) | `membership` |
|
|
605
702
|
| 7 | idempotency | `idempotency.model`, `idempotency.services.[name]` |
|
|
606
703
|
| 8 | traffic | `traffic.model`, `traffic.throttling` |
|
|
607
704
|
| 9 | cache | `cache.model`, `cache.defaultTTL`, `cache.services.[name]` |
|
|
@@ -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"
|
|
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
|
|
package/docs/middleware/mt.md
CHANGED
|
@@ -515,6 +515,7 @@ The `soajsinjectobj` header contains:
|
|
|
515
515
|
"lastName": "Doe",
|
|
516
516
|
"email": "user@example.com",
|
|
517
517
|
"groups": ["group1"],
|
|
518
|
+
"membership": "whale", // If resolved, see custom registry `membership`
|
|
518
519
|
"tenant": { "id": "...", "code": "..." },
|
|
519
520
|
"profile": { ... }, // If urac_Profile enabled
|
|
520
521
|
"acl": { ... }, // If urac_ACL enabled
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
/**
|
|
12
|
+
* Resolve the membership the caller holds for the product this request is made under.
|
|
13
|
+
*
|
|
14
|
+
* A membership is held per main product on the user record, and only its name is
|
|
15
|
+
* stored. What the name grants is resolved from the catalog, it is never copied
|
|
16
|
+
* onto the user:
|
|
17
|
+
*
|
|
18
|
+
* "memberships": [{ "product": "AVAPP", "membership": "whale" }]
|
|
19
|
+
*
|
|
20
|
+
* A request carries the product of the key it was made with. That product is either
|
|
21
|
+
* the main product itself or one of its sub products, and both resolve to the main
|
|
22
|
+
* product, so a call made under CLUBS reads the AVAPP membership.
|
|
23
|
+
*
|
|
24
|
+
* Configured at registry.custom.gateway.value.membership, keyed by main product:
|
|
25
|
+
*
|
|
26
|
+
* {
|
|
27
|
+
* "active": true,
|
|
28
|
+
* "AVAPP": ["CLUBS", "CORPS", "SECRT", "GRPCH"]
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* NOTE: this fails closed. An absent or inactive block, an unconfigured product, or
|
|
32
|
+
* a user with no entry for the resolved main product all resolve to null, and
|
|
33
|
+
* nothing is injected, so no service sees a membership it should not.
|
|
34
|
+
*
|
|
35
|
+
* @param config the custom registry membership block
|
|
36
|
+
* @param product the product the request is made under, ex: keyObj.application.product
|
|
37
|
+
* @param memberships the memberships array off the user record
|
|
38
|
+
* @returns {string|null} the membership name, or null when it does not resolve
|
|
39
|
+
*/
|
|
40
|
+
function resolveMembership(config, product, memberships) {
|
|
41
|
+
if (!config || !config.active) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!product || !Array.isArray(memberships) || memberships.length === 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//NOTE: product codes are matched exactly, they are uppercase by convention and
|
|
49
|
+
// normalizing here would hide a miscased entry in the registry
|
|
50
|
+
let mainProduct = null;
|
|
51
|
+
let configuredProducts = Object.keys(config).filter((key) => {
|
|
52
|
+
return key !== "active";
|
|
53
|
+
});
|
|
54
|
+
for (let i = 0; i < configuredProducts.length; i++) {
|
|
55
|
+
let key = configuredProducts[i];
|
|
56
|
+
if (key === product || (Array.isArray(config[key]) && config[key].includes(product))) {
|
|
57
|
+
mainProduct = key;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!mainProduct) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < memberships.length; i++) {
|
|
66
|
+
let entry = memberships[i];
|
|
67
|
+
if (entry && entry.product === mainProduct && entry.membership) {
|
|
68
|
+
return entry.membership;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { resolveMembership };
|
package/mw/lastSeen/index.js
CHANGED
|
@@ -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.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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/mw/mt/index.js
CHANGED
|
@@ -14,6 +14,9 @@ const utils = require("./utils");
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
const { encodeHeaderValue } = require("../../lib/header.js");
|
|
17
|
+
const { resolveMembership } = require("../../lib/membership.js");
|
|
18
|
+
|
|
19
|
+
const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
*
|
|
@@ -266,6 +269,15 @@ module.exports = (configuration) => {
|
|
|
266
269
|
}
|
|
267
270
|
};
|
|
268
271
|
|
|
272
|
+
//NOTE: only the resolved name travels, never the raw per product
|
|
273
|
+
// array, so a service cannot read a membership held for
|
|
274
|
+
// a product this request was not made under
|
|
275
|
+
let membershipConfig = get(["soajs", "registry", "custom", "gateway", "value", "membership"], req);
|
|
276
|
+
let membership = resolveMembership(membershipConfig, keyObj.application.product, uracObj.memberships);
|
|
277
|
+
if (membership) {
|
|
278
|
+
injectObj.urac.membership = membership;
|
|
279
|
+
}
|
|
280
|
+
|
|
269
281
|
injectObj.param = injectObj.param || {};
|
|
270
282
|
injectObj.param.urac_Profile = serviceParam.urac_Profile;
|
|
271
283
|
injectObj.param.urac_ACL = serviceParam.urac_ACL;
|
package/mw/mt/urac.js
CHANGED
|
@@ -167,6 +167,13 @@ Urac.prototype.getProfile = function (_ALL) {
|
|
|
167
167
|
"tenant": _self.userRecord.tenant
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
+
//NOTE: the raw per product array. The gateway resolves the one entry that applies
|
|
171
|
+
// to this request and injects only that, see lib/membership.js. Set only when
|
|
172
|
+
// the record carries it, so a user with no memberships keeps the shape it had.
|
|
173
|
+
if (_self.userRecord.memberships) {
|
|
174
|
+
urac.memberships = _self.userRecord.memberships;
|
|
175
|
+
}
|
|
176
|
+
|
|
170
177
|
if (_self.userRecord.socialLogin) {
|
|
171
178
|
urac.socialLogin = {
|
|
172
179
|
"strategy": _self.userRecord.socialLogin.strategy,
|
package/package.json
CHANGED
package/test/unit/index.js
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
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 assert = require('assert');
|
|
12
|
+
const helper = require("../../helper.js");
|
|
13
|
+
|
|
14
|
+
const { resolveMembership } = helper.requireModule('./lib/membership.js');
|
|
15
|
+
|
|
16
|
+
describe("Unit test for: lib - membership", () => {
|
|
17
|
+
|
|
18
|
+
const CONFIG = {
|
|
19
|
+
"active": true,
|
|
20
|
+
"AVAPP": ["CLUBS", "CORPS", "SECRT", "GRPCH"]
|
|
21
|
+
};
|
|
22
|
+
const MEMBERSHIPS = [{ "product": "AVAPP", "membership": "whale" }];
|
|
23
|
+
|
|
24
|
+
describe("resolving", () => {
|
|
25
|
+
|
|
26
|
+
it("resolves when the request is made under the main product", (done) => {
|
|
27
|
+
assert.strictEqual(resolveMembership(CONFIG, "AVAPP", MEMBERSHIPS), "whale");
|
|
28
|
+
done();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("resolves when the request is made under a sub product", (done) => {
|
|
32
|
+
["CLUBS", "CORPS", "SECRT", "GRPCH"].forEach((sub) => {
|
|
33
|
+
assert.strictEqual(resolveMembership(CONFIG, sub, MEMBERSHIPS), "whale", "failed for " + sub);
|
|
34
|
+
});
|
|
35
|
+
done();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("resolves the main product itself when it has no sub products", (done) => {
|
|
39
|
+
assert.strictEqual(resolveMembership({ "active": true, "AVAPP": [] }, "AVAPP", MEMBERSHIPS), "whale");
|
|
40
|
+
done();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("picks the entry for the resolved main product, not the first one", (done) => {
|
|
44
|
+
let memberships = [
|
|
45
|
+
{ "product": "OTHER", "membership": "minnow" },
|
|
46
|
+
{ "product": "AVAPP", "membership": "whale" }
|
|
47
|
+
];
|
|
48
|
+
assert.strictEqual(resolveMembership(CONFIG, "CLUBS", memberships), "whale");
|
|
49
|
+
done();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("keeps several main products independent", (done) => {
|
|
53
|
+
let config = {
|
|
54
|
+
"active": true,
|
|
55
|
+
"AVAPP": ["CLUBS"],
|
|
56
|
+
"BVAPP": ["BOATS"]
|
|
57
|
+
};
|
|
58
|
+
let memberships = [
|
|
59
|
+
{ "product": "AVAPP", "membership": "whale" },
|
|
60
|
+
{ "product": "BVAPP", "membership": "dolphin" }
|
|
61
|
+
];
|
|
62
|
+
assert.strictEqual(resolveMembership(config, "CLUBS", memberships), "whale");
|
|
63
|
+
assert.strictEqual(resolveMembership(config, "BOATS", memberships), "dolphin");
|
|
64
|
+
done();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("failing closed", () => {
|
|
69
|
+
|
|
70
|
+
it("resolves nothing when the block is absent", (done) => {
|
|
71
|
+
assert.strictEqual(resolveMembership(null, "AVAPP", MEMBERSHIPS), null);
|
|
72
|
+
assert.strictEqual(resolveMembership(undefined, "AVAPP", MEMBERSHIPS), null);
|
|
73
|
+
done();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("resolves nothing when active is not set", (done) => {
|
|
77
|
+
assert.strictEqual(resolveMembership({ "AVAPP": ["CLUBS"] }, "AVAPP", MEMBERSHIPS), null);
|
|
78
|
+
done();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("resolves nothing when active is false", (done) => {
|
|
82
|
+
assert.strictEqual(resolveMembership({ "active": false, "AVAPP": ["CLUBS"] }, "CLUBS", MEMBERSHIPS), null);
|
|
83
|
+
done();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("resolves nothing when no product is configured", (done) => {
|
|
87
|
+
assert.strictEqual(resolveMembership({ "active": true }, "AVAPP", MEMBERSHIPS), null);
|
|
88
|
+
done();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("resolves nothing for a product that is neither main nor sub", (done) => {
|
|
92
|
+
assert.strictEqual(resolveMembership(CONFIG, "UNRELATED", MEMBERSHIPS), null);
|
|
93
|
+
done();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("resolves nothing when the user holds no entry for the main product", (done) => {
|
|
97
|
+
let memberships = [{ "product": "OTHER", "membership": "minnow" }];
|
|
98
|
+
assert.strictEqual(resolveMembership(CONFIG, "CLUBS", memberships), null);
|
|
99
|
+
done();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("resolves nothing when the user has no memberships", (done) => {
|
|
103
|
+
assert.strictEqual(resolveMembership(CONFIG, "AVAPP", null), null);
|
|
104
|
+
assert.strictEqual(resolveMembership(CONFIG, "AVAPP", []), null);
|
|
105
|
+
assert.strictEqual(resolveMembership(CONFIG, "AVAPP", undefined), null);
|
|
106
|
+
done();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("resolves nothing when the request carries no product", (done) => {
|
|
110
|
+
assert.strictEqual(resolveMembership(CONFIG, null, MEMBERSHIPS), null);
|
|
111
|
+
done();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("matches product codes exactly, a miscased code does not resolve", (done) => {
|
|
115
|
+
assert.strictEqual(resolveMembership(CONFIG, "clubs", MEMBERSHIPS), null);
|
|
116
|
+
assert.strictEqual(resolveMembership(CONFIG, "AVAPP", [{ "product": "avapp", "membership": "whale" }]), null);
|
|
117
|
+
done();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("malformed input", () => {
|
|
122
|
+
|
|
123
|
+
it("survives a memberships array holding junk", (done) => {
|
|
124
|
+
let memberships = [null, "whale", { "product": "AVAPP" }, { "membership": "whale" }];
|
|
125
|
+
assert.strictEqual(resolveMembership(CONFIG, "AVAPP", memberships), null);
|
|
126
|
+
done();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("skips an entry with no membership name and keeps looking", (done) => {
|
|
130
|
+
let memberships = [{ "product": "AVAPP" }, { "product": "AVAPP", "membership": "whale" }];
|
|
131
|
+
assert.strictEqual(resolveMembership(CONFIG, "AVAPP", memberships), "whale");
|
|
132
|
+
done();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("survives a sub product list that is not an array", (done) => {
|
|
136
|
+
assert.strictEqual(resolveMembership({ "active": true, "AVAPP": "CLUBS" }, "CLUBS", MEMBERSHIPS), null);
|
|
137
|
+
assert.strictEqual(resolveMembership({ "active": true, "AVAPP": "CLUBS" }, "AVAPP", MEMBERSHIPS), "whale");
|
|
138
|
+
done();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("does not treat active as a product", (done) => {
|
|
142
|
+
assert.strictEqual(resolveMembership({ "active": true }, "active", [{ "product": "active", "membership": "x" }]), null);
|
|
143
|
+
done();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -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
|
});
|