scimgateway 4.2.5 → 4.2.7
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/README.md +38 -15
- package/config/plugin-api.json +2 -1
- package/config/plugin-azure-ad.json +2 -1
- package/config/plugin-ldap.json +2 -1
- package/config/plugin-loki.json +2 -1
- package/config/plugin-mongodb.json +2 -1
- package/config/plugin-mssql.json +2 -1
- package/config/plugin-saphana.json +2 -1
- package/config/plugin-scim.json +2 -1
- package/config/{plugin-forwardinc.json → plugin-soap.json} +3 -2
- package/index.js +1 -1
- package/lib/plugin-api.js +1 -9
- package/lib/plugin-azure-ad.js +10 -12
- package/lib/plugin-mongodb.js +46 -21
- package/lib/plugin-scim.js +2 -10
- package/lib/{plugin-forwardinc.js → plugin-soap.js} +1 -1
- package/lib/postinstall.js +2 -2
- package/lib/scimgateway.js +147 -111
- package/package.json +60 -60
package/README.md
CHANGED
|
@@ -64,9 +64,10 @@ Can be used as SCIM version-gateway e.g. 1.1=>2.0 or 2.0=>1.1
|
|
|
64
64
|
Can be used to chain several SCIM Gateway's
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
* **
|
|
68
|
-
Demonstrates user provisioning towards SOAP-Based endpoint
|
|
69
|
-
|
|
67
|
+
* **Soap** (SOAP Webservice)
|
|
68
|
+
Demonstrates user provisioning towards SOAP-Based endpoint
|
|
69
|
+
Excample WSDLs are included
|
|
70
|
+
Using endpoint "Forwardinc" as an example (comes with Symantec/Broadcom/CA IM SDK - SDKWS)
|
|
70
71
|
Shows how to implement a highly configurable multi tenant or multi endpoint solution through `baseEntity` in URL
|
|
71
72
|
|
|
72
73
|
* **MSSQL** (MSSQL Database)
|
|
@@ -188,7 +189,7 @@ When maintaining a set of modifications it useful to disable the postinstall ope
|
|
|
188
189
|
const loki = require('./lib/plugin-loki')
|
|
189
190
|
// const mongodb = require('./lib/plugin-mongodb')
|
|
190
191
|
// const scim = require('./lib/plugin-scim')
|
|
191
|
-
// const
|
|
192
|
+
// const soap = require('./lib/plugin-soap')
|
|
192
193
|
// const mssql = require('./lib/plugin-mssql')
|
|
193
194
|
// const saphana = require('./lib/plugin-saphana') // prereq: npm install hdb
|
|
194
195
|
// const azureAD = require('./lib/plugin-azure-ad')
|
|
@@ -212,7 +213,8 @@ Below shows an example of config\plugin-saphana.json
|
|
|
212
213
|
"version": "2.0",
|
|
213
214
|
"customSchema": null,
|
|
214
215
|
"skipTypeConvert" : false,
|
|
215
|
-
"usePutSoftSync" : false
|
|
216
|
+
"usePutSoftSync" : false,
|
|
217
|
+
"usePutGroupMemberOfUser": false
|
|
216
218
|
},
|
|
217
219
|
"log": {
|
|
218
220
|
"loglevel": {
|
|
@@ -342,7 +344,9 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
342
344
|
]
|
|
343
345
|
|
|
344
346
|
|
|
345
|
-
- **scim.usePutSoftSync** - true or false, default false. `PUT /Users/bjensen` will replace the user bjensen with body content. If body contains groups, usePutSoftsync=true will prevent removing any existing groups that are not included in body.groups
|
|
347
|
+
- **scim.usePutSoftSync** - true or false, default false. `PUT /Users/bjensen` will replace the user bjensen with body content. If body contains groups, usePutSoftsync=true will prevent removing any existing groups that are not included in body.groups
|
|
348
|
+
|
|
349
|
+
- **scim."usePutGroupMemberOfUser** - true or false, default false. `PUT /Users/<user>` will replace the user with body content. If body contains groups and usePutGroupMemberOfUser=true, groups will be set on user object (groups are member of user) instead of default user member of groups
|
|
346
350
|
|
|
347
351
|
- **log.loglevel.file** - off, error, info, or debug. Output to plugin-logfile e.g. `logs\plugin-saphana.log`
|
|
348
352
|
|
|
@@ -462,20 +466,20 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
462
466
|
}
|
|
463
467
|
|
|
464
468
|
|
|
465
|
-
secrets.json for plugin-
|
|
469
|
+
secrets.json for plugin-soap - example (dot notation):
|
|
466
470
|
|
|
467
471
|
{
|
|
468
|
-
"plugin-
|
|
469
|
-
"plugin-
|
|
470
|
-
"plugin-
|
|
471
|
-
"plugin-
|
|
472
|
+
"plugin-soap.scimgateway.auth.basic[0].username": "gwadmin",
|
|
473
|
+
"plugin-soap.scimgateway.auth.basic[0].password": "password",
|
|
474
|
+
"plugin-soap.endpoint.username": "superuser",
|
|
475
|
+
"plugin-soap.endpoint.password": "secret"
|
|
472
476
|
}
|
|
473
477
|
|
|
474
478
|
- Custom schema attributes can be added by plugin configuration `scim.customSchema` having value set to filename of a JSON schema-file located in `<package-root>/config/schemas` e.g:
|
|
475
479
|
|
|
476
480
|
"scim": {
|
|
477
481
|
"version": "2.0",
|
|
478
|
-
"customSchema": "plugin-
|
|
482
|
+
"customSchema": "plugin-soap-schema.json"
|
|
479
483
|
},
|
|
480
484
|
|
|
481
485
|
JSON file have following syntax:
|
|
@@ -748,7 +752,7 @@ Username, password and port must correspond with plugin configuration file. For
|
|
|
748
752
|
http://localhost:8880/client-a
|
|
749
753
|
http://localhost:8880/client-b
|
|
750
754
|
|
|
751
|
-
Each baseEntity should then be defined in the plugin configuration file with custom attributes needed. Please see examples in plugin-
|
|
755
|
+
Each baseEntity should then be defined in the plugin configuration file with custom attributes needed. Please see examples in plugin-soap.json
|
|
752
756
|
|
|
753
757
|
IM 12.6 SP7 (and above) also supports pagination for SCIM endpoint (data transferred in bulks - endpoint explore of users). Loki plugin supports pagination. Other plugin may ignore this setting.
|
|
754
758
|
|
|
@@ -965,7 +969,7 @@ For JavaScript coding editor you may use [Visual Studio Code](https://code.visua
|
|
|
965
969
|
|
|
966
970
|
Preparation:
|
|
967
971
|
|
|
968
|
-
* Copy "best matching" example plugin e.g. `lib\plugin-mssql.js` and `config\plugin-mssql.json` and rename both copies to your plugin name prefix e.g. plugin-mine.js and plugin-mine.json (for SOAP Webservice endpoint we might use plugin-
|
|
972
|
+
* Copy "best matching" example plugin e.g. `lib\plugin-mssql.js` and `config\plugin-mssql.json` and rename both copies to your plugin name prefix e.g. plugin-mine.js and plugin-mine.json (for SOAP Webservice endpoint we might use plugin-soap as a template)
|
|
969
973
|
* Edit plugin-mine.json and define a unique port number for the gateway setting
|
|
970
974
|
* Edit index.js and add a new line for starting your plugin e.g. `let mine = require('./lib/plugin-mine');`
|
|
971
975
|
* Start SCIM Gateway and verify. If using CA Provisioning you could setup a SCIM endpoint using the port number you defined
|
|
@@ -988,7 +992,7 @@ Please see plugin-saphana that do not use groups.
|
|
|
988
992
|
|
|
989
993
|
Template used by CA Provisioning role should only include endpoint supported attributes defined in our plugin. Template should therefore have no links to global user for none supported attributes (e.g. remove %UT% from "Job Title" if our endpoint/code do not support title)
|
|
990
994
|
|
|
991
|
-
CA Provisioning using default SCIM endpoint do not support SCIM Enterprise User Schema Extension (having attributes like employeeNumber, costCenter, organization, division, department and manager). If we need these or other attributes not found in CA Provisioning, we could define our own by using the free-text "type" definition in the multivalue entitlements or roles attribute. In the template entitlements definition, we could for example define type=Company and set value to %UCOMP%. Please see plugin-
|
|
995
|
+
CA Provisioning using default SCIM endpoint do not support SCIM Enterprise User Schema Extension (having attributes like employeeNumber, costCenter, organization, division, department and manager). If we need these or other attributes not found in CA Provisioning, we could define our own by using the free-text "type" definition in the multivalue entitlements or roles attribute. In the template entitlements definition, we could for example define type=Company and set value to %UCOMP%. Please see plugin-soap.js using Company as a multivalue "type" definition.
|
|
992
996
|
|
|
993
997
|
Using CA Connector Xpress we could create a new SCIM endpoint type based on the original SCIM. We could then add/remove attributes and change from default assign "user to groups" to assign "groups to user". There are also other predefined endpoints based on the original SCIM. You may take a look at "ServiceNow - WSL7" and "Zendesk - WSL7".
|
|
994
998
|
|
|
@@ -1165,6 +1169,25 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1165
1169
|
|
|
1166
1170
|
## Change log
|
|
1167
1171
|
|
|
1172
|
+
### v4.2.7
|
|
1173
|
+
|
|
1174
|
+
[Added]
|
|
1175
|
+
|
|
1176
|
+
- new plugin configuration **scim.usePutGroupMemberOfUser** can be set to true or false, default false. `PUT /Users/<user>` will replace the user bjensen with body content. If body contains groups and usePutGroupMemberOfUser=true, groups will be set on user object (groups are member of user) instead of default user member of groups
|
|
1177
|
+
- plugin-forwardinc renamed to plugin-soap
|
|
1178
|
+
- Dependencies bump
|
|
1179
|
+
|
|
1180
|
+
[Fixed]
|
|
1181
|
+
|
|
1182
|
+
- plugin-azure-ad fixed some issues introduced in v4.2.4
|
|
1183
|
+
- plugin-mongodb fixed some issues introduced in v4.2.4
|
|
1184
|
+
|
|
1185
|
+
### v4.2.6
|
|
1186
|
+
|
|
1187
|
+
[Fixed]
|
|
1188
|
+
|
|
1189
|
+
- cosmetics related to 401 error handling introduced in v4.2.4
|
|
1190
|
+
|
|
1168
1191
|
### v4.2.5
|
|
1169
1192
|
|
|
1170
1193
|
[Fixed]
|
package/config/plugin-api.json
CHANGED
package/config/plugin-ldap.json
CHANGED
package/config/plugin-loki.json
CHANGED
package/config/plugin-mssql.json
CHANGED
package/config/plugin-scim.json
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"version": "2.0",
|
|
8
8
|
"customSchema": null,
|
|
9
9
|
"skipTypeConvert": false,
|
|
10
|
-
"usePutSoftSync": false
|
|
10
|
+
"usePutSoftSync": false,
|
|
11
|
+
"usePutGroupMemberOfUser": false
|
|
11
12
|
},
|
|
12
13
|
"log": {
|
|
13
14
|
"loglevel": {
|
|
@@ -156,4 +157,4 @@
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
|
-
}
|
|
160
|
+
}
|
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const loki = require('./lib/plugin-loki')
|
|
13
13
|
// const mongodb = require('./lib/plugin-mongodb')
|
|
14
14
|
// const scim = require('./lib/plugin-scim')
|
|
15
|
-
// const
|
|
15
|
+
// const soap = require('./lib/plugin-soap')
|
|
16
16
|
// const mssql = require('./lib/plugin-mssql')
|
|
17
17
|
// const saphana = require('./lib/plugin-saphana') // prereq: npm install hdb --save
|
|
18
18
|
// const azureAD = require('./lib/plugin-azure-ad')
|
package/lib/plugin-api.js
CHANGED
|
@@ -434,15 +434,7 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
|
|
|
434
434
|
throw newerr
|
|
435
435
|
}
|
|
436
436
|
} else {
|
|
437
|
-
if (statusCode === 401)
|
|
438
|
-
if (_serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
|
|
439
|
-
err.message = JSON.stringify( // don't reveal original message
|
|
440
|
-
{
|
|
441
|
-
statusCode: 401,
|
|
442
|
-
error: 'Access denied'
|
|
443
|
-
}
|
|
444
|
-
)
|
|
445
|
-
}
|
|
437
|
+
if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
|
|
446
438
|
throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
|
|
447
439
|
}
|
|
448
440
|
}
|
package/lib/plugin-azure-ad.js
CHANGED
|
@@ -578,7 +578,9 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
578
578
|
} else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
|
|
579
579
|
// mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
|
|
580
580
|
// Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
|
|
581
|
-
|
|
581
|
+
// not using below expand because Azure returns only a maximum of 20 items for the expanded relationship
|
|
582
|
+
// path = `/users/${getObj.value}/memberOf/microsoft.graph.group?$select=id,displayName&$expand=members($select=id,displayName)`
|
|
583
|
+
path = `/users/${getObj.value}/memberOf/microsoft.graph.group?$select=id,displayName`
|
|
582
584
|
} else {
|
|
583
585
|
// optional - simpel filtering
|
|
584
586
|
throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
|
|
@@ -633,6 +635,10 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
633
635
|
}
|
|
634
636
|
})
|
|
635
637
|
delete response.body.value[i].members
|
|
638
|
+
} else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') { // Not using expand-members. Only includes current user as member, but should have requested all...
|
|
639
|
+
members = [{
|
|
640
|
+
value: getObj.value
|
|
641
|
+
}]
|
|
636
642
|
}
|
|
637
643
|
|
|
638
644
|
const [scimObj] = scimgateway.endpointMapper('inbound', response.body.value[i], config.map.group) // endpoint => SCIM/CustomSCIM attribute standard
|
|
@@ -1160,15 +1166,7 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
|
|
|
1160
1166
|
throw newerr
|
|
1161
1167
|
}
|
|
1162
1168
|
} else {
|
|
1163
|
-
if (statusCode === 401)
|
|
1164
|
-
if (_serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
|
|
1165
|
-
err.message = JSON.stringify( // don't reveal original message
|
|
1166
|
-
{
|
|
1167
|
-
statusCode: 401,
|
|
1168
|
-
error: 'Access denied'
|
|
1169
|
-
}
|
|
1170
|
-
)
|
|
1171
|
-
}
|
|
1169
|
+
if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
|
|
1172
1170
|
throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
|
|
1173
1171
|
}
|
|
1174
1172
|
}
|
|
@@ -1182,9 +1180,9 @@ const getAccessToken = async (baseEntity, ctx) => {
|
|
|
1182
1180
|
const clientIdentifier = getClientIdentifier(ctx)
|
|
1183
1181
|
const d = new Date() / 1000 // seconds (unix time)
|
|
1184
1182
|
if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken &&
|
|
1185
|
-
(_serviceClient[baseEntity].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
|
|
1183
|
+
(_serviceClient[baseEntity][clientIdentifier].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
|
|
1186
1184
|
lock.release()
|
|
1187
|
-
return _serviceClient[baseEntity].accessToken
|
|
1185
|
+
return _serviceClient[baseEntity][clientIdentifier].accessToken
|
|
1188
1186
|
}
|
|
1189
1187
|
|
|
1190
1188
|
const action = 'getAccessToken'
|
package/lib/plugin-mongodb.js
CHANGED
|
@@ -48,15 +48,16 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
|
|
|
48
48
|
|
|
49
49
|
const validFilterOperators = ['eq', 'ne', 'aeq', 'dteq', 'gt', 'gte', 'lt', 'lte', 'between', 'jgt', 'jgte', 'jlt', 'jlte', 'jbetween', 'regex', 'in', 'nin', 'keyin', 'nkeyin', 'definedin', 'undefinedin', 'contains', 'containsAny', 'type', 'finite', 'size', 'len', 'exists']
|
|
50
50
|
|
|
51
|
-
if (!config.entity) throw new Error('error: configuration entity is missing')
|
|
52
|
-
if (!scimgateway.authPassThroughAllowed) { // not using Auth PassThrough, loading db handler at startup using username/password from config
|
|
53
|
-
for (const baseEntity in config.entity) {
|
|
54
|
-
loadHandler(baseEntity)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
51
|
async function loadHandler (baseEntity, ctx) {
|
|
59
52
|
const action = 'loadHander'
|
|
53
|
+
|
|
54
|
+
const clientIdentifier = getClientIdentifier(ctx)
|
|
55
|
+
if (config.entity[baseEntity].isLoaded) { // loadHandler only once
|
|
56
|
+
if (!clientIdentifier) return clientIdentifier // not using Auth PassThrough
|
|
57
|
+
if (config.entity[baseEntity][clientIdentifier]) return clientIdentifier // authenticated
|
|
58
|
+
throw new Error('{"error":"Access denied","statusCode":401}') // string: "statusCode":401 ensure gateway returns 401
|
|
59
|
+
}
|
|
60
|
+
|
|
60
61
|
if (!config.entity[baseEntity].baseUrl) { // mongodb://host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]] - e.g: mongodb://localhost:27017/db?tls=true&tlsInsecure=true
|
|
61
62
|
throw new Error(`${action} error: configuration entity.${baseEntity}.baseUrl is missing`)
|
|
62
63
|
}
|
|
@@ -102,6 +103,9 @@ async function loadHandler (baseEntity, ctx) {
|
|
|
102
103
|
groups.createIndex({ id: 1 }, { unique: true })
|
|
103
104
|
}
|
|
104
105
|
} catch (error) {
|
|
106
|
+
if (clientIdentifier && error.message.includes('Authentication')) {
|
|
107
|
+
throw new Error('{"error":"Access denied","statusCode":401}') // string: "statusCode":401 ensure gateway returns 401
|
|
108
|
+
}
|
|
105
109
|
throw new Error(`${action} error: failed to connect to database '${client.s.options.dbName}' - ${error.message}`)
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -147,11 +151,12 @@ async function loadHandler (baseEntity, ctx) {
|
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
|
-
const clientIdentifier = getClientIdentifier(ctx)
|
|
151
154
|
if (!config.entity[baseEntity][clientIdentifier]) config.entity[baseEntity][clientIdentifier] = {}
|
|
152
155
|
config.entity[baseEntity][clientIdentifier].collection = {}
|
|
153
156
|
config.entity[baseEntity][clientIdentifier].collection.users = users
|
|
154
157
|
config.entity[baseEntity][clientIdentifier].collection.groups = groups
|
|
158
|
+
config.entity[baseEntity].isLoaded = true
|
|
159
|
+
return clientIdentifier
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
// =================================================
|
|
@@ -173,6 +178,8 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
173
178
|
const action = 'getUsers'
|
|
174
179
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
|
|
175
180
|
|
|
181
|
+
const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
|
|
182
|
+
|
|
176
183
|
if (getObj.operator) { // convert to plugin supported syntax
|
|
177
184
|
switch (getObj.operator) {
|
|
178
185
|
case 'co':
|
|
@@ -203,10 +210,6 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
203
210
|
}
|
|
204
211
|
}
|
|
205
212
|
|
|
206
|
-
const clientIdentifier = getClientIdentifier(ctx)
|
|
207
|
-
if (ctx && !config.entity[baseEntity][clientIdentifier]) { // first (or previous failed) PassThrough attempt - have to load connection
|
|
208
|
-
await loadHandler(baseEntity, ctx)
|
|
209
|
-
}
|
|
210
213
|
const users = config.entity[baseEntity][clientIdentifier].collection.users
|
|
211
214
|
let findObj
|
|
212
215
|
|
|
@@ -275,6 +278,8 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
|
|
|
275
278
|
const action = 'createUser'
|
|
276
279
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" userObj=${JSON.stringify(userObj)}`)
|
|
277
280
|
|
|
281
|
+
const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
|
|
282
|
+
|
|
278
283
|
const notValid = scimgateway.notValidAttributes(userObj, validScimAttr) // We should check for unsupported endpoint attributes
|
|
279
284
|
if (notValid) {
|
|
280
285
|
throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
|
|
@@ -308,7 +313,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
|
|
|
308
313
|
userObj = encodeDotDate(userObj)
|
|
309
314
|
|
|
310
315
|
try {
|
|
311
|
-
const users = config.entity[baseEntity].collection.users
|
|
316
|
+
const users = config.entity[baseEntity][clientIdentifier].collection.users
|
|
312
317
|
await users.insertOne(userObj)
|
|
313
318
|
return null
|
|
314
319
|
} catch (err) {
|
|
@@ -327,7 +332,9 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
|
|
|
327
332
|
const action = 'deleteUser'
|
|
328
333
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
|
|
329
334
|
|
|
330
|
-
const
|
|
335
|
+
const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
|
|
336
|
+
|
|
337
|
+
const users = config.entity[baseEntity][clientIdentifier].collection.users
|
|
331
338
|
try {
|
|
332
339
|
/*
|
|
333
340
|
const now = Date.now()
|
|
@@ -354,6 +361,8 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
354
361
|
const action = 'modifyUser'
|
|
355
362
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
356
363
|
|
|
364
|
+
const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
|
|
365
|
+
|
|
357
366
|
const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr) // We should check for unsupported endpoint attributes
|
|
358
367
|
if (notValid) {
|
|
359
368
|
throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
|
|
@@ -363,7 +372,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
363
372
|
let res
|
|
364
373
|
|
|
365
374
|
try {
|
|
366
|
-
const users = config.entity[baseEntity].collection.users
|
|
375
|
+
const users = config.entity[baseEntity][clientIdentifier].collection.users
|
|
367
376
|
res = await users.find({ id }, { projection: { _id: 0 } }).toArray()
|
|
368
377
|
if (res.length === 0) throw new Error('user does not exist')
|
|
369
378
|
if (res.length > 1) throw new Error('user is not unique, more than one have been found')
|
|
@@ -474,7 +483,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
474
483
|
userObj = encodeDotDate(userObj)
|
|
475
484
|
|
|
476
485
|
try {
|
|
477
|
-
const users = config.entity[baseEntity].collection.users
|
|
486
|
+
const users = config.entity[baseEntity][clientIdentifier].collection.users
|
|
478
487
|
await users.replaceOne({ id: id }, userObj)
|
|
479
488
|
return null
|
|
480
489
|
} catch (err) {
|
|
@@ -501,6 +510,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
501
510
|
const action = 'getGroups'
|
|
502
511
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
|
|
503
512
|
|
|
513
|
+
const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
|
|
514
|
+
|
|
504
515
|
if (getObj.operator) { // convert to plugin supported syntax
|
|
505
516
|
switch (getObj.operator) {
|
|
506
517
|
case 'co':
|
|
@@ -574,7 +585,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
574
585
|
|
|
575
586
|
try {
|
|
576
587
|
const projection = attributes.length > 0 ? getProjectionFromAttributes(attributes) : { _id: 0 }
|
|
577
|
-
const groups = config.entity[baseEntity].collection.groups
|
|
588
|
+
const groups = config.entity[baseEntity][clientIdentifier].collection.groups
|
|
578
589
|
const groupsArr = await groups.find(findObj, { projection: projection }).sort({ _id: 1 }).skip(getObj.startIndex - 1).limit(getObj.count).toArray()
|
|
579
590
|
const totalResults = await groups.countDocuments(findObj, { projection: projection })
|
|
580
591
|
const arr = groupsArr.map((obj) => {
|
|
@@ -595,6 +606,8 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
|
|
|
595
606
|
const action = 'createGroup'
|
|
596
607
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
|
|
597
608
|
|
|
609
|
+
const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
|
|
610
|
+
|
|
598
611
|
if (!groupObj.meta) {
|
|
599
612
|
const now = Date.now()
|
|
600
613
|
groupObj.meta = {
|
|
@@ -608,7 +621,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
|
|
|
608
621
|
groupObj = encodeDotDate(groupObj)
|
|
609
622
|
|
|
610
623
|
try {
|
|
611
|
-
const groups = config.entity[baseEntity].collection.groups
|
|
624
|
+
const groups = config.entity[baseEntity][clientIdentifier].collection.groups
|
|
612
625
|
await groups.insertOne(groupObj)
|
|
613
626
|
return null
|
|
614
627
|
} catch (err) {
|
|
@@ -627,7 +640,9 @@ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
|
|
|
627
640
|
const action = 'deleteGroup'
|
|
628
641
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
|
|
629
642
|
|
|
630
|
-
const
|
|
643
|
+
const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
|
|
644
|
+
|
|
645
|
+
const groups = config.entity[baseEntity][clientIdentifier].collection.groups
|
|
631
646
|
try {
|
|
632
647
|
/*
|
|
633
648
|
const now = Date.now()
|
|
@@ -654,8 +669,10 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
654
669
|
const action = 'modifyGroup'
|
|
655
670
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
656
671
|
|
|
657
|
-
const
|
|
658
|
-
|
|
672
|
+
const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
|
|
673
|
+
|
|
674
|
+
const users = config.entity[baseEntity][clientIdentifier].collection.users
|
|
675
|
+
const groups = config.entity[baseEntity][clientIdentifier].collection.groups
|
|
659
676
|
let res
|
|
660
677
|
let isModified = false
|
|
661
678
|
|
|
@@ -851,3 +868,11 @@ process.on('SIGINT', () => {
|
|
|
851
868
|
}
|
|
852
869
|
}
|
|
853
870
|
})
|
|
871
|
+
|
|
872
|
+
// connect MongoDb and load users/groups
|
|
873
|
+
if (!config.entity) throw new Error('error: configuration entity is missing')
|
|
874
|
+
if (!scimgateway.authPassThroughAllowed) { // not using Auth PassThrough, loading db handler at startup using username/password from config
|
|
875
|
+
for (const baseEntity in config.entity) {
|
|
876
|
+
loadHandler(baseEntity)
|
|
877
|
+
}
|
|
878
|
+
}
|
package/lib/plugin-scim.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// =================================================================================
|
|
2
|
-
// File: plugin-
|
|
2
|
+
// File: plugin-scim.js
|
|
3
3
|
//
|
|
4
4
|
// Author: Jarle Elshaug
|
|
5
5
|
//
|
|
@@ -773,15 +773,7 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
|
|
|
773
773
|
throw newerr
|
|
774
774
|
}
|
|
775
775
|
} else {
|
|
776
|
-
if (statusCode === 401)
|
|
777
|
-
if (_serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
|
|
778
|
-
err.message = JSON.stringify( // don't reveal original message
|
|
779
|
-
{
|
|
780
|
-
statusCode: 401,
|
|
781
|
-
error: 'Access denied'
|
|
782
|
-
}
|
|
783
|
-
)
|
|
784
|
-
}
|
|
776
|
+
if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
|
|
785
777
|
throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
|
|
786
778
|
}
|
|
787
779
|
}
|
package/lib/postinstall.js
CHANGED
|
@@ -29,7 +29,7 @@ if (!fsExistsSync('../../lib')) fs.mkdirSync('../../lib')
|
|
|
29
29
|
|
|
30
30
|
if (!fsExistsSync('../../config/plugin-loki.json')) fs.writeFileSync('../../config/plugin-loki.json', fs.readFileSync('./config/plugin-loki.json'))
|
|
31
31
|
if (!fsExistsSync('../../config/plugin-scim.json')) fs.writeFileSync('../../config/plugin-scim.json', fs.readFileSync('./config/plugin-scim.json'))
|
|
32
|
-
if (!fsExistsSync('../../config/plugin-
|
|
32
|
+
if (!fsExistsSync('../../config/plugin-soap.json')) fs.writeFileSync('../../config/plugin-soap.json', fs.readFileSync('./config/plugin-soap.json'))
|
|
33
33
|
if (!fsExistsSync('../../config/plugin-mssql.json')) fs.writeFileSync('../../config/plugin-mssql.json', fs.readFileSync('./config/plugin-mssql.json'))
|
|
34
34
|
if (!fsExistsSync('../../config/plugin-saphana.json')) fs.writeFileSync('../../config/plugin-saphana.json', fs.readFileSync('./config/plugin-saphana.json'))
|
|
35
35
|
if (!fsExistsSync('../../config/plugin-api.json')) fs.writeFileSync('../../config/plugin-api.json', fs.readFileSync('./config/plugin-api.json'))
|
|
@@ -39,7 +39,7 @@ if (!fsExistsSync('../../config/plugin-mongodb.json')) fs.writeFileSync('../../c
|
|
|
39
39
|
|
|
40
40
|
fs.writeFileSync('../../lib/plugin-loki.js', fs.readFileSync('./lib/plugin-loki.js'))
|
|
41
41
|
fs.writeFileSync('../../lib/plugin-scim.js', fs.readFileSync('./lib/plugin-scim.js'))
|
|
42
|
-
fs.writeFileSync('../../lib/plugin-
|
|
42
|
+
fs.writeFileSync('../../lib/plugin-soap.js', fs.readFileSync('./lib/plugin-soap.js'))
|
|
43
43
|
fs.writeFileSync('../../lib/plugin-mssql.js', fs.readFileSync('./lib/plugin-mssql.js'))
|
|
44
44
|
fs.writeFileSync('../../lib/plugin-saphana.js', fs.readFileSync('./lib/plugin-saphana.js'))
|
|
45
45
|
fs.writeFileSync('../../lib/plugin-api.js', fs.readFileSync('./lib/plugin-api.js'))
|
package/lib/scimgateway.js
CHANGED
|
@@ -318,11 +318,27 @@ const ScimGateway = function () {
|
|
|
318
318
|
if (!userName && authType === 'Bearer') userName = 'token'
|
|
319
319
|
if (ctx.request.url !== '/favicon.ico') {
|
|
320
320
|
if (ctx.response.status < 200 || ctx.response.status > 299) {
|
|
321
|
+
let isEndpointAccessDenied = false
|
|
322
|
+
if (res.body.detail) {
|
|
323
|
+
if (res.body.detail.includes('\"statusCode\":401')) isEndpointAccessDenied= true // eslint-disable-line
|
|
324
|
+
} else if (res.body.Errors) {
|
|
325
|
+
if (Array.isArray(res.body.Errors) && res.body.Errors[0].description && res.body.Errors[0].description.includes('\"statusCode\":401')) { // eslint-disable-line
|
|
326
|
+
isEndpointAccessDenied = true
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (isEndpointAccessDenied) { // don't reveal original SCIM error message details related to access denied (e.g. using Auth PassThrough)
|
|
330
|
+
ctx.response.set('Content-Type', 'application/json; charset=utf-8')
|
|
331
|
+
ctx.response.status = 401 // ctx.response.message becomes default 'Unauthorized'
|
|
332
|
+
ctx.response.body = { error: 'Access denied' }
|
|
333
|
+
res.statusCode = ctx.response.status
|
|
334
|
+
res.statusMessage = ctx.response.message
|
|
335
|
+
res.body = ctx.response.body
|
|
336
|
+
}
|
|
321
337
|
logger.error(`${gwName}[${pluginName}] ${ellapsed} ${ctx.request.ipcli} ${userName} ${ctx.request.method} ${ctx.request.href} Inbound = ${JSON.stringify(ctx.request.body)} Outbound = ${JSON.stringify(res)}${(config.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
|
|
322
338
|
} else logger.info(`${gwName}[${pluginName}] ${ellapsed} ${ctx.request.ipcli} ${userName} ${ctx.request.method} ${ctx.request.href} Inbound = ${JSON.stringify(ctx.request.body)} Outbound = ${JSON.stringify(res)}${(config.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
|
|
323
339
|
requestCounter += 1 // logged on exit (not win process termination)
|
|
324
340
|
}
|
|
325
|
-
if (ctx.response.body && typeof ctx.response.body === 'object') ctx.set('Content-Type', 'application/scim+json; charset=utf-8')
|
|
341
|
+
if (ctx.response.body && typeof ctx.response.body === 'object' && ctx.response.status !== 401) ctx.set('Content-Type', 'application/scim+json; charset=utf-8')
|
|
326
342
|
}
|
|
327
343
|
|
|
328
344
|
// start auth methods - used by auth
|
|
@@ -1274,22 +1290,22 @@ const ScimGateway = function () {
|
|
|
1274
1290
|
}
|
|
1275
1291
|
logger.debug(`${gwName}[${pluginName}] calling "${handle.getMethod}" and awaiting result`)
|
|
1276
1292
|
const res = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'id', operator: 'eq', value: id }, ctx.query.attributes ? ctx.query.attributes.split(',').map(item => item.trim()) : [], ctx.ctxCopy)
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1293
|
+
|
|
1294
|
+
if (!res || !res.Resources || !Array.isArray(res.Resources)) {
|
|
1295
|
+
throw new Error(`using ${handle.getMethod} to retrive ${id} after ${handle.modifyMethod} - got invalid response: ${res}`)
|
|
1280
1296
|
}
|
|
1281
|
-
if (res) {
|
|
1282
|
-
|
|
1283
|
-
scimdata.Resources = res.Resources
|
|
1284
|
-
scimdata.totalResults = res.totalResults
|
|
1285
|
-
} else if (Array.isArray(res)) scimdata.Resources = res
|
|
1286
|
-
else if (typeof (res) === 'object' && Object.keys(res).length > 0) scimdata.Resources[0] = res
|
|
1297
|
+
if (res.Resources.length > 1) {
|
|
1298
|
+
throw new Error(`using ${handle.getMethod} to retrive ${id} after ${handle.modifyMethod} - response returned ${res.Resources.length} objects`)
|
|
1287
1299
|
}
|
|
1288
|
-
if (
|
|
1300
|
+
if (res.Resources.length === 0) {
|
|
1301
|
+
ctx.status = 204
|
|
1302
|
+
return
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1289
1305
|
const location = ctx.origin + ctx.path
|
|
1290
1306
|
ctx.set('Location', location)
|
|
1291
|
-
|
|
1292
|
-
scimdata = utils.stripObj(
|
|
1307
|
+
res.Resources[0] = addPrimaryAttrs(res.Resources[0])
|
|
1308
|
+
scimdata = utils.stripObj(res.Resources[0], ctx.query.attributes, ctx.query.excludedAttributes)
|
|
1293
1309
|
scimdata = addSchemas(scimdata, handle.description, isScimv2)
|
|
1294
1310
|
ctx.status = 200
|
|
1295
1311
|
ctx.body = scimdata
|
|
@@ -1346,6 +1362,16 @@ const ScimGateway = function () {
|
|
|
1346
1362
|
}
|
|
1347
1363
|
}
|
|
1348
1364
|
}
|
|
1365
|
+
if (config.scim.usePutGroupMemberOfUser) { // group member of user instead of default user member of group
|
|
1366
|
+
if (clearedObj.groups && Array.isArray(clearedObj.groups)) {
|
|
1367
|
+
for (let i = 0; i < clearedObj.groups.length; i++) {
|
|
1368
|
+
if (clearedObj.groups[i].operation && clearedObj.groups[i].operation === 'delete') {
|
|
1369
|
+
clearedObj.groups.splice(i, 1) // delete
|
|
1370
|
+
i -= 1
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1349
1375
|
}
|
|
1350
1376
|
|
|
1351
1377
|
// merge cleared object with the new
|
|
@@ -1353,9 +1379,9 @@ const ScimGateway = function () {
|
|
|
1353
1379
|
delete newObj.id
|
|
1354
1380
|
delete newObj.userName
|
|
1355
1381
|
delete newObj.externalId
|
|
1356
|
-
delete newObj.groups // do not support "group member of users"
|
|
1357
1382
|
delete newObj.schemas
|
|
1358
1383
|
delete newObj.meta
|
|
1384
|
+
if (!config.scim.usePutGroupMemberOfUser) delete newObj.groups
|
|
1359
1385
|
if (handle.getMethod === handler.groups.getMethod) delete newObj.displayName
|
|
1360
1386
|
|
|
1361
1387
|
let [scimdata, err] = ScimGateway.prototype.convertedScim(newObj)
|
|
@@ -1366,125 +1392,135 @@ const ScimGateway = function () {
|
|
|
1366
1392
|
await this[handle.modifyMethod](ctx.params.baseEntity, id, scimdata, ctx.ctxCopy)
|
|
1367
1393
|
|
|
1368
1394
|
// add/remove groups
|
|
1369
|
-
if (
|
|
1370
|
-
if (
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
let currentGroups
|
|
1374
|
-
if (currentObj.groups && Array.isArray(currentObj.groups)) currentGroups = currentObj.groups
|
|
1375
|
-
else { // try to get current groups the standard way
|
|
1376
|
-
let res
|
|
1377
|
-
try {
|
|
1378
|
-
res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }, ['id', 'displayName'], ctx.ctxCopy)
|
|
1379
|
-
} catch (err) {} // method may be implemented but throwing error like groups not supported/implemented
|
|
1380
|
-
currentGroups = []
|
|
1381
|
-
if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
|
|
1382
|
-
for (let i = 0; i < res.Resources.length; i++) {
|
|
1383
|
-
if (!res.Resources[i].id) continue
|
|
1384
|
-
const el = {}
|
|
1385
|
-
el.value = res.Resources[i].id
|
|
1386
|
-
if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
|
|
1387
|
-
currentGroups.push(el) // { "value": "Admins", "display": "Admins"}
|
|
1388
|
-
}
|
|
1395
|
+
if (!config.scim.usePutGroupMemberOfUser) { // default user member of group
|
|
1396
|
+
if (jsonBody.groups && Array.isArray(jsonBody.groups)) { // only if groups included, { "groups": [] } will remove all existing
|
|
1397
|
+
if (typeof this[handler.groups.getMethod] !== 'function' || typeof this[handler.groups.modifyMethod] !== 'function') {
|
|
1398
|
+
throw new Error('replaceUser error: put operation can not be fully completed for the user`s groups, methods like getGroups() and modifyGroup() are not implemented')
|
|
1389
1399
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1400
|
+
let currentGroups
|
|
1401
|
+
if (currentObj.groups && Array.isArray(currentObj.groups)) currentGroups = currentObj.groups
|
|
1402
|
+
else { // try to get current groups the standard way
|
|
1403
|
+
let res
|
|
1404
|
+
try {
|
|
1405
|
+
res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }, ['id', 'displayName'], ctx.ctxCopy)
|
|
1406
|
+
} catch (err) {} // method may be implemented but throwing error like groups not supported/implemented
|
|
1407
|
+
currentGroups = []
|
|
1408
|
+
if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
|
|
1409
|
+
for (let i = 0; i < res.Resources.length; i++) {
|
|
1410
|
+
if (!res.Resources[i].id) continue
|
|
1411
|
+
const el = {}
|
|
1412
|
+
el.value = res.Resources[i].id
|
|
1413
|
+
if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
|
|
1414
|
+
currentGroups.push(el) // { "value": "Admins", "display": "Admins"}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1394
1417
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1418
|
+
currentGroups = currentGroups.map((el) => {
|
|
1419
|
+
if (el.value) {
|
|
1420
|
+
el.value = decodeURIComponent(el.value)
|
|
1421
|
+
}
|
|
1422
|
+
return el
|
|
1423
|
+
})
|
|
1397
1424
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1425
|
+
const addGrps = []
|
|
1426
|
+
const removeGrps = []
|
|
1427
|
+
// add
|
|
1428
|
+
for (let i = 0; i < jsonBody.groups.length; i++) {
|
|
1429
|
+
if (!jsonBody.groups[i].value) continue
|
|
1430
|
+
jsonBody.groups[i].value = decodeURIComponent(jsonBody.groups[i].value)
|
|
1431
|
+
let found = false
|
|
1432
|
+
for (let j = 0; j < currentGroups.length; j++) {
|
|
1433
|
+
if (jsonBody.groups[i].value === currentGroups[j].value) {
|
|
1434
|
+
found = true
|
|
1435
|
+
break
|
|
1436
|
+
}
|
|
1409
1437
|
}
|
|
1438
|
+
if (!found && jsonBody.groups[i].value) addGrps.push(jsonBody.groups[i].value)
|
|
1410
1439
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
break
|
|
1440
|
+
// remove
|
|
1441
|
+
for (let i = 0; i < currentGroups.length; i++) {
|
|
1442
|
+
let found = false
|
|
1443
|
+
for (let j = 0; j < jsonBody.groups.length; j++) {
|
|
1444
|
+
if (!jsonBody.groups[j].value) continue
|
|
1445
|
+
jsonBody.groups[j].value = decodeURIComponent(jsonBody.groups[j].value)
|
|
1446
|
+
if (currentGroups[i].value === jsonBody.groups[j].value) {
|
|
1447
|
+
found = true
|
|
1448
|
+
break
|
|
1449
|
+
}
|
|
1422
1450
|
}
|
|
1451
|
+
if (!found && currentGroups[i].value) removeGrps.push(currentGroups[i].value)
|
|
1423
1452
|
}
|
|
1424
|
-
if (!found && currentGroups[i].value) removeGrps.push(currentGroups[i].value)
|
|
1425
|
-
}
|
|
1426
1453
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1454
|
+
const addGroups = async (grp) => {
|
|
1455
|
+
const obj = { members: [{ value: id }] }
|
|
1456
|
+
return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj, ctx.ctxCopy)
|
|
1457
|
+
}
|
|
1431
1458
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1459
|
+
const removeGroups = async (grp) => {
|
|
1460
|
+
const obj = { members: [{ operation: 'delete', value: id }] }
|
|
1461
|
+
return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj, ctx.ctxCopy)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
let errRemove
|
|
1465
|
+
if (!config.scim.usePutSoftSync) { // default will remove any existing groups not included, usePutSoftSync=true prevents removing existing groups (only add groups)
|
|
1466
|
+
await Promise.all(removeGrps.map((grp) => removeGroups(grp)))
|
|
1467
|
+
.then()
|
|
1468
|
+
.catch((err) => {
|
|
1469
|
+
errRemove = err
|
|
1470
|
+
})
|
|
1471
|
+
}
|
|
1436
1472
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
await Promise.all(removeGrps.map((grp) => removeGroups(grp)))
|
|
1473
|
+
let errAdd
|
|
1474
|
+
await Promise.all(addGrps.map((grp) => addGroups(grp)))
|
|
1440
1475
|
.then()
|
|
1441
1476
|
.catch((err) => {
|
|
1442
|
-
|
|
1477
|
+
errAdd = err
|
|
1443
1478
|
})
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
let errAdd
|
|
1447
|
-
await Promise.all(addGrps.map((grp) => addGroups(grp)))
|
|
1448
|
-
.then()
|
|
1449
|
-
.catch((err) => {
|
|
1450
|
-
errAdd = err
|
|
1451
|
-
})
|
|
1452
1479
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1480
|
+
let errMsg = ''
|
|
1481
|
+
if (errRemove) errMsg = `removeGroups error: ${errRemove.message}`
|
|
1482
|
+
if (errAdd) errMsg += `${errMsg ? ' ' : ''}addGroups error: ${errAdd.message}`
|
|
1483
|
+
if (errMsg) throw new Error(errMsg)
|
|
1484
|
+
}
|
|
1457
1485
|
}
|
|
1458
1486
|
|
|
1459
1487
|
// get updated object
|
|
1460
1488
|
logger.debug(`${gwName}[${pluginName}] calling "${handle.getMethod}" and awaiting result`)
|
|
1461
1489
|
res = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'id', operator: 'eq', value: id }, [], ctx.ctxCopy)
|
|
1462
1490
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1491
|
+
if (!res || !res.Resources || !Array.isArray(res.Resources)) {
|
|
1492
|
+
throw new Error(`put using method ${handle.getMethod} - got unexpected response: ${JSON.stringify(res)}`)
|
|
1493
|
+
}
|
|
1494
|
+
if (res.Resources.length > 1) {
|
|
1495
|
+
throw new Error(`put using method ${handle.getMethod} to retrive ${id} - response returned ${res.Resources.length} objects`)
|
|
1496
|
+
}
|
|
1497
|
+
if (res.Resources.length === 0) {
|
|
1498
|
+
ctx.status = 204
|
|
1499
|
+
return
|
|
1500
|
+
}
|
|
1501
|
+
scimdata = res.Resources[0]
|
|
1468
1502
|
|
|
1469
1503
|
// include groups
|
|
1470
|
-
if (
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1504
|
+
if (!config.scim.usePutGroupMemberOfUser) { // default user member of group
|
|
1505
|
+
if (handle.getMethod === handler.users.getMethod && typeof this[handler.groups.getMethod] === 'function') {
|
|
1506
|
+
logger.debug(`${gwName}[${pluginName}] calling "${handler.groups.getMethod}" and awaiting result`)
|
|
1507
|
+
const res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: id }, ['id', 'displayName'], ctx.ctxCopy)
|
|
1508
|
+
let grps = []
|
|
1509
|
+
if (res && res.Resources && Array.isArray(res.Resources)) grps = res.Resources
|
|
1510
|
+
else if (Array.isArray(res)) grps = res
|
|
1511
|
+
else if (res && typeof (res) === 'object' && Object.keys(res).length > 0) grps = [res]
|
|
1512
|
+
else throw Error(`put using method ${handler.groups.getMethod} - got unexpected response: ${JSON.stringify(res)}`)
|
|
1513
|
+
|
|
1514
|
+
if (grps.length > 0) {
|
|
1515
|
+
scimdata.groups = []
|
|
1516
|
+
for (let i = 0; i < grps.length; i++) {
|
|
1517
|
+
const el = {}
|
|
1518
|
+
el.value = grps[i].id
|
|
1519
|
+
if (grps[i].displayName) el.display = grps[i].displayName
|
|
1520
|
+
if (isScimv2) el.type = 'direct'
|
|
1521
|
+
else el.type = { value: 'direct' }
|
|
1522
|
+
if (el.value) scimdata.groups.push(el) // { "value": "Admins", "display": "Admins", "type": "direct"}
|
|
1523
|
+
}
|
|
1488
1524
|
}
|
|
1489
1525
|
}
|
|
1490
1526
|
}
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "scimgateway",
|
|
3
|
-
"version": "4.2.
|
|
4
|
-
"description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
|
|
5
|
-
"author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
|
|
6
|
-
"homepage": "https://elshaug.xyz",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"main": "lib/scimgateway.js",
|
|
9
|
-
"scripts": {
|
|
10
|
-
"postinstall": "node lib/postinstall.js",
|
|
11
|
-
"start": "node index.js",
|
|
12
|
-
"test": "mocha -R spec ./test"
|
|
13
|
-
},
|
|
14
|
-
"bin": {
|
|
15
|
-
"scimgateway": "./index.js"
|
|
16
|
-
},
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "https://github.com/jelhub/scimgateway.git"
|
|
20
|
-
},
|
|
21
|
-
"keywords": [
|
|
22
|
-
"scim",
|
|
23
|
-
"gateway",
|
|
24
|
-
"proxy",
|
|
25
|
-
"azure",
|
|
26
|
-
"identity",
|
|
27
|
-
"manager",
|
|
28
|
-
"provisioning",
|
|
29
|
-
"iga"
|
|
30
|
-
],
|
|
31
|
-
"engines": {
|
|
32
|
-
"node": ">=14.0.0"
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"@godaddy/terminus": "^4.
|
|
36
|
-
"callsite": "^1.0.0",
|
|
37
|
-
"dot-object": "^2.1.4",
|
|
38
|
-
"https-proxy-agent": "^5.0.1",
|
|
39
|
-
"is-in-subnet": "^4.0.1",
|
|
40
|
-
"jsonwebtoken": "^9.0.0",
|
|
41
|
-
"koa": "^2.14.
|
|
42
|
-
"koa-bodyparser": "^4.
|
|
43
|
-
"koa-router": "^12.0.0",
|
|
44
|
-
"ldapjs": "^
|
|
45
|
-
"lokijs": "^1.5.12",
|
|
46
|
-
"mongodb": "^
|
|
47
|
-
"node-machine-id": "1.1.9",
|
|
48
|
-
"nodemailer": "^6.
|
|
49
|
-
"passport": "^0.6.0",
|
|
50
|
-
"passport-azure-ad": "^4.3.
|
|
51
|
-
"soap": "^0.
|
|
52
|
-
"tedious": "^
|
|
53
|
-
"winston": "^3.
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"chai": "^4.2.0",
|
|
57
|
-
"mocha": "^9.2.0",
|
|
58
|
-
"supertest": "^
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "scimgateway",
|
|
3
|
+
"version": "4.2.7",
|
|
4
|
+
"description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
|
|
5
|
+
"author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
|
|
6
|
+
"homepage": "https://elshaug.xyz",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "lib/scimgateway.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node lib/postinstall.js",
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"test": "mocha -R spec ./test"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"scimgateway": "./index.js"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/jelhub/scimgateway.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"scim",
|
|
23
|
+
"gateway",
|
|
24
|
+
"proxy",
|
|
25
|
+
"azure",
|
|
26
|
+
"identity",
|
|
27
|
+
"manager",
|
|
28
|
+
"provisioning",
|
|
29
|
+
"iga"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=14.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@godaddy/terminus": "^4.12.0",
|
|
36
|
+
"callsite": "^1.0.0",
|
|
37
|
+
"dot-object": "^2.1.4",
|
|
38
|
+
"https-proxy-agent": "^5.0.1",
|
|
39
|
+
"is-in-subnet": "^4.0.1",
|
|
40
|
+
"jsonwebtoken": "^9.0.0",
|
|
41
|
+
"koa": "^2.14.2",
|
|
42
|
+
"koa-bodyparser": "^4.4.0",
|
|
43
|
+
"koa-router": "^12.0.0",
|
|
44
|
+
"ldapjs": "^3.0.2",
|
|
45
|
+
"lokijs": "^1.5.12",
|
|
46
|
+
"mongodb": "^5.6.0",
|
|
47
|
+
"node-machine-id": "1.1.9",
|
|
48
|
+
"nodemailer": "^6.9.3",
|
|
49
|
+
"passport": "^0.6.0",
|
|
50
|
+
"passport-azure-ad": "^4.3.5",
|
|
51
|
+
"soap": "^1.0.0",
|
|
52
|
+
"tedious": "^16.1.0",
|
|
53
|
+
"winston": "^3.9.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"chai": "^4.2.0",
|
|
57
|
+
"mocha": "^9.2.0",
|
|
58
|
+
"supertest": "^6.3.3"
|
|
59
|
+
}
|
|
60
|
+
}
|