scimgateway 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.travis.yml +1 -1
- package/README.md +37 -7
- package/config/{plugin-restful.json → plugin-scim.json} +0 -0
- package/index.js +1 -1
- package/lib/plugin-ldap.js +310 -78
- package/lib/plugin-loki.js +8 -6
- package/lib/plugin-mongodb.js +7 -6
- package/lib/{plugin-restful.js → plugin-scim.js} +0 -0
- package/lib/postinstall.js +16 -8
- package/lib/scimgateway.js +42 -20
- package/lib/utils.js +6 -0
- package/package.json +4 -4
- package/test/index.js +2 -2
- package/test/lib/{plugin-restful.js → plugin-scim.js} +2 -2
package/.travis.yml
CHANGED
package/README.md
CHANGED
|
@@ -38,8 +38,6 @@ Using Identity Manager, we could setup one or more endpoints of type SCIM pointi
|
|
|
38
38
|
|
|
39
39
|

|
|
40
40
|
|
|
41
|
-
Instead of using IM-SDK for building our own integration for none supported endpoints, we can now build new integration based on SCIM Gateway plugins. SCIM Gateway works with IM as long as IM supports SCIM.
|
|
42
|
-
|
|
43
41
|
SCIM Gateway is based on the popular asynchronous event driven framework [Node.js](https://nodejs.dev/) using JavaScript. It is firewall friendly using REST webservices. Runs on almost all operating systems, and may load balance between hosts (horizontal) and cpu's (vertical). Could even be uploaded and run as a cloud application.
|
|
44
42
|
|
|
45
43
|
**Following example plugins are included:**
|
|
@@ -56,12 +54,15 @@ Example of a fully functional SCIM Gateway plugin
|
|
|
56
54
|
Same as plugin "Loki" but using MongoDB
|
|
57
55
|
Shows how to implement a highly configurable multi tenant or multi endpoint solution through `baseEntity` in URL
|
|
58
56
|
|
|
59
|
-
* **
|
|
60
|
-
Demonstrates user provisioning towards
|
|
61
|
-
Using plugin "Loki" as
|
|
57
|
+
* **SCIM** (REST Webservice)
|
|
58
|
+
Demonstrates user provisioning towards a SCIM endpoint using REST
|
|
59
|
+
Using plugin "Loki" as SCIM endpoint
|
|
60
|
+
Can be used as SCIM version-gateway e.g. 1.1=>2.0 or 2.0=>1.1
|
|
61
|
+
Can be used to chain several SCIM Gateway's
|
|
62
|
+
|
|
62
63
|
|
|
63
64
|
* **Forwardinc** (SOAP Webservice)
|
|
64
|
-
Demonstrates
|
|
65
|
+
Demonstrates provisioning towards SOAP-Based endpoint
|
|
65
66
|
Using endpoint Forwardinc that comes with Broadcom/CA IM SDK (SDKWS) - [wiki.ca.com](https://docops.ca.com/ca-identity-manager/12-6-8/EN/programming/connector-programming-reference/sdk-sample-connectors/sdkws-sdk-web-services-connector/sdkws-sample-connector-build-requirements "wiki.ca.com")
|
|
66
67
|
Shows how to implement a highly configurable multi tenant or multi endpoint solution through `baseEntity` in URL
|
|
67
68
|
|
|
@@ -137,7 +138,7 @@ If internet connection is blocked, we could install on another machine and copy
|
|
|
137
138
|
|
|
138
139
|
http://localhost:8880/Groups?filter=displayName eq "Admins"&excludedAttributes=members
|
|
139
140
|
http://localhost:8880/Users?filter=userName eq "bjensen"&attributes=userName,id,name.givenName
|
|
140
|
-
http://localhost:8880/Users?filter=meta.created
|
|
141
|
+
http://localhost:8880/Users?filter=meta.created ge "2010-01-01T00:00:00Z"&attributes=userName,name.familyName,meta.created
|
|
141
142
|
http://localhost:8880/Users?filter=emails.value co "@example.com"&attributes=userName,name.familyName,emails&sortBy=name.familyName&sortOrder=descending
|
|
142
143
|
=> Filtering examples
|
|
143
144
|
|
|
@@ -173,6 +174,10 @@ Note, always backup/copy C:\\my-scimgateway before upgrading. Custom plugins and
|
|
|
173
174
|
|
|
174
175
|
To force a major upgrade (version x.\*.\* => y.\*.\*) that will brake compability with any existing custom plugins, we have to include the `@latest` suffix in the install command: `npm install scimgateway@latest`
|
|
175
176
|
|
|
177
|
+
##### Avoid (re-)adding the files created during `postinstall`
|
|
178
|
+
|
|
179
|
+
When maintaining a set of modifications it useful to disable the postinstall operations to keep your changes intact by setting the property `scimgateway_postinstall_skip = true` in `.npmrc` or by setting environment `SCIMGATEWAY_POSTINSTALL_SKIP = true`
|
|
180
|
+
|
|
176
181
|
## Configuration
|
|
177
182
|
|
|
178
183
|
**index.js** defines one or more plugins to be started. We could comment out those we do not need. Default configuration only starts the loki plugin.
|
|
@@ -385,6 +390,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
385
390
|
- Setting environment variable `SEED` will override default password seeding logic.
|
|
386
391
|
- All configuration can be set based on environment variables. Syntax will then be `"process.env.<ENVIRONMENT>"` where `<ENVIRONMENT>` is the environment variable used. E.g. scimgateway.port could have value "process.env.PORT", then using environment variable PORT.
|
|
387
392
|
- All configuration can be set based on corresponding JSON-content (dot notation) in external file using plugin name as parent JSON object. Syntax will then be `"process.file.<path>"` where `<path>` is the file used. E.g. endpoint.password could have value "process.file./var/run/vault/secrets.json"
|
|
393
|
+
- Indivudual Secrets can be contained in plain text files. Syntax will then be `"process.text.<path>"` where `<path>` is the file which contains raw (`UTF-8`) character value. E.g. endpoint.password could have value "process.text./var/run/vault/endpoint.password". This enables that the config file itself be loaded from a ConfigMap while specific values are mounted either from `secrets.json` style files as mentioned above OR from traditional secrets files mounted in the file system, one value per file.
|
|
388
394
|
|
|
389
395
|
Example:
|
|
390
396
|
|
|
@@ -404,6 +410,11 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
404
410
|
},
|
|
405
411
|
...
|
|
406
412
|
],
|
|
413
|
+
"bearerJwt": [
|
|
414
|
+
"secret": "process.text./var/run/vault/jwt.secret",
|
|
415
|
+
"publicKey": "process.text./var/run/vault/jwt.pub",
|
|
416
|
+
...
|
|
417
|
+
],
|
|
407
418
|
...
|
|
408
419
|
},
|
|
409
420
|
"endpoint": {
|
|
@@ -1117,6 +1128,25 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1117
1128
|
|
|
1118
1129
|
## Change log
|
|
1119
1130
|
|
|
1131
|
+
### v4.0.1
|
|
1132
|
+
[Added]
|
|
1133
|
+
|
|
1134
|
+
- create user/group supporting externalId
|
|
1135
|
+
- plugin-restful renamed to plugin-scim
|
|
1136
|
+
- plugin-ldap having improved SID/GUID support for Active Directory, also supporting domain map of userPrincipalName e.g. Azure AD => Active Directory
|
|
1137
|
+
|
|
1138
|
+
"userPrincipalName": {
|
|
1139
|
+
"mapTo": "userName",
|
|
1140
|
+
"type": "string",
|
|
1141
|
+
"mapDomain": {
|
|
1142
|
+
"inbound": "test.onmicrosoft.com",
|
|
1143
|
+
"outbound": "my-company.com"
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
- postinstall copying example plugins may be skipped by setting the property `scimgateway_postinstall_skip = true` in `.npmrc` or by setting environment `SCIMGATEWAY_POSTINSTALL_SKIP = true`
|
|
1147
|
+
- Secrets now also support key-value storage. The key defined in plugin configuration have syntax `process.text.<path>` where `<path>` is the file which contains raw (UTF-8) character value. E.g. configuration `endpoint.password` could have value `process.text./var/run/vault/endpoint.password`, and the corresponding file contains the secret. **Thanks to Raymond Augé**
|
|
1148
|
+
|
|
1149
|
+
|
|
1120
1150
|
### v4.0.0
|
|
1121
1151
|
**[MAJOR]**
|
|
1122
1152
|
|
|
File without changes
|
package/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const loki = require('./lib/plugin-loki')
|
|
13
13
|
// const mongodb = require('./lib/plugin-mongodb')
|
|
14
|
-
// const
|
|
14
|
+
// const scim = require('./lib/plugin-scim')
|
|
15
15
|
// const forwardinc = require('./lib/plugin-forwardinc')
|
|
16
16
|
// const mssql = require('./lib/plugin-mssql')
|
|
17
17
|
// const saphana = require('./lib/plugin-saphana') // prereq: npm install hdb --save
|
package/lib/plugin-ldap.js
CHANGED
|
@@ -5,9 +5,24 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Purpose: General ldap plugin having plugin-ldap.json configured for Active Directory.
|
|
7
7
|
// Using endpointMapper for attribute flexibility. Includes some special logic
|
|
8
|
-
// for Active Directory
|
|
9
|
-
// and objectGUID. objectGUID can
|
|
10
|
-
//
|
|
8
|
+
// for Active Directory attributes like userAccountControl, unicodePW
|
|
9
|
+
// and objectSid/objectGUID. objectSid/objectGUID can be used in mapper configuration.
|
|
10
|
+
// e.g: replacing config.map.user.dn and config.map.group.dn with
|
|
11
|
+
// config.map.user.objectSid or config.map.user.objectGUID e.g:
|
|
12
|
+
//
|
|
13
|
+
// "objectSid": {
|
|
14
|
+
// "mapTo": "id",
|
|
15
|
+
// "type": "string"
|
|
16
|
+
// },
|
|
17
|
+
// "objectGUID": {
|
|
18
|
+
// "mapTo": "userName",
|
|
19
|
+
// "type": "string"
|
|
20
|
+
// },
|
|
21
|
+
// "userPrincipalName": {
|
|
22
|
+
// "mapTo": "externalId",
|
|
23
|
+
// "type": "string"
|
|
24
|
+
// }
|
|
25
|
+
//
|
|
11
26
|
//
|
|
12
27
|
// Attributes according to map definition in the configuration file plugin-ldap.json:
|
|
13
28
|
//
|
|
@@ -67,6 +82,37 @@ config = scimgateway.processExtConfig(pluginName, config) // add any external co
|
|
|
67
82
|
|
|
68
83
|
const _serviceClient = {}
|
|
69
84
|
|
|
85
|
+
if (!config.map || !config.map.user) {
|
|
86
|
+
scimgateway.logger.error(`${pluginName} map.user configuration is mandatory`)
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
config.useSID_id = config.map.user.objectSid && config.map.user.objectSid.mapTo === 'id' // AD proprietary SID/GUID
|
|
91
|
+
config.useGUID_id = config.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id'
|
|
92
|
+
if (config.useSID_id && config.map.group) {
|
|
93
|
+
if (!config.map.group.objectSid || config.map.group.objectSid.mapTo !== 'id') {
|
|
94
|
+
scimgateway.logger.error(`${pluginName} missing configuration group.objectSid - user and group should be using the same attribute`)
|
|
95
|
+
process.exit(1)
|
|
96
|
+
}
|
|
97
|
+
} else if (config.useGUID_id && config.map.group) {
|
|
98
|
+
if (!config.map.group.objectGUID || config.map.group.objectGUID.mapTo !== 'id') {
|
|
99
|
+
scimgateway.logger.error(`${pluginName} missing configuration group.objectGUID - user and group should be using the same attribute`)
|
|
100
|
+
process.exit(1)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) { // support mapping different inbound/outbound upn domain names
|
|
104
|
+
if (config.map.user.userPrincipalName.mapDomain.inbound && config.map.user.userPrincipalName.mapDomain.outbound) {
|
|
105
|
+
let inbound = config.map.user.userPrincipalName.mapDomain.inbound
|
|
106
|
+
let outbound = config.map.user.userPrincipalName.mapDomain.outbound
|
|
107
|
+
inbound = inbound.startsWith('@') ? inbound : '@' + inbound
|
|
108
|
+
outbound = outbound.startsWith('@') ? outbound : '@' + outbound
|
|
109
|
+
config.upnMapDomain = {
|
|
110
|
+
inbound: inbound, // "test.onmicrosoft.com
|
|
111
|
+
outbound: outbound // "my-company.com"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
// =================================================
|
|
71
117
|
// getUsers
|
|
72
118
|
// =================================================
|
|
@@ -110,19 +156,40 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes) => {
|
|
|
110
156
|
if (getObj.operator) {
|
|
111
157
|
if (getObj.operator === 'eq' && ['id', 'userName', 'externalId'].includes(getObj.attribute)) {
|
|
112
158
|
// mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
|
|
113
|
-
if (getObj.attribute === 'id') { // using dn or objectGUID (Active Directory)
|
|
114
|
-
if (config.
|
|
115
|
-
|
|
159
|
+
if (getObj.attribute === 'id') { // lookup using dn or objectSid/objectGUID (Active Directory)
|
|
160
|
+
if (config.useSID_id) {
|
|
161
|
+
const sid = convertStringToSid(getObj.value) // sid using formatted string instead of default hex
|
|
162
|
+
if (!sid) throw new Error(`${action} error: ${getObj.attribute}=${getObj.value} - attribute having a none valid SID string`)
|
|
163
|
+
base = `<SID=${sid}>`
|
|
164
|
+
} else if (config.useGUID_id) {
|
|
165
|
+
const guid = Buffer.from(getObj.value, 'base64').toString('hex')
|
|
166
|
+
base = `<GUID=${guid}>` // '<GUID=b3975b675d3a21498b4e511e1a8ccb9e>'
|
|
167
|
+
} else base = getObj.value
|
|
116
168
|
ldapOptions = {
|
|
117
169
|
attributes: attrs
|
|
118
170
|
}
|
|
119
171
|
} else {
|
|
120
172
|
const [userIdAttr, err] = scimgateway.endpointMapper('outbound', getObj.attribute, config.map.user) // e.g. 'userName' => 'sAMAccountName'
|
|
121
173
|
if (err) throw new Error(`${action} error: ${err.message}`)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
174
|
+
if (userIdAttr === 'objectSid') {
|
|
175
|
+
const sid = convertStringToSid(getObj.value)
|
|
176
|
+
if (!sid) throw new Error(`${action} error: ${getObj.attribute}=${getObj.value} - attribute having a none valid SID string`)
|
|
177
|
+
base = `<SID=${sid}>`
|
|
178
|
+
ldapOptions = {
|
|
179
|
+
attributes: attrs
|
|
180
|
+
}
|
|
181
|
+
} else if (userIdAttr === 'objectGUID') {
|
|
182
|
+
const guid = Buffer.from(getObj.value, 'base64').toString('hex')
|
|
183
|
+
base = `<GUID=${guid}>`
|
|
184
|
+
ldapOptions = {
|
|
185
|
+
attributes: attrs
|
|
186
|
+
}
|
|
187
|
+
} else { // search instead of lookup
|
|
188
|
+
ldapOptions = {
|
|
189
|
+
filter: `&${getObjClassFilter(baseEntity, 'user')}(${userIdAttr}=${getObj.value})`, // &(objectClass=user)(objectClass=person)(objectClass=organizationalPerson)(objectClass=top)(sAMAccountName=bjensen)
|
|
190
|
+
scope: scope,
|
|
191
|
+
attributes: attrs
|
|
192
|
+
}
|
|
126
193
|
}
|
|
127
194
|
}
|
|
128
195
|
} else if (getObj.operator === 'eq' && getObj.attribute === 'group.value') {
|
|
@@ -163,20 +230,20 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes) => {
|
|
|
163
230
|
|
|
164
231
|
if (user.memberOf) {
|
|
165
232
|
if (!config.map.group) user.memberOf = [] // empty any values
|
|
166
|
-
else if (config.
|
|
233
|
+
else if (config.useSID_id || config.useGUID_id) { // Active Directory - convert memberOf having dn values to objectSid/objectGUID
|
|
167
234
|
const arr = []
|
|
168
235
|
try {
|
|
169
236
|
if (Array.isArray(user.memberOf)) {
|
|
170
237
|
for (let i = 0; i < user.memberOf.length; i++) {
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
173
|
-
arr.push(
|
|
238
|
+
const id = await dnToSidGuid(baseEntity, user.memberOf[i])
|
|
239
|
+
if (!id) throw new Error(`dnToGuid did not return any objectGUID value for dn=${user.memberOf[i]}`)
|
|
240
|
+
arr.push(id)
|
|
174
241
|
}
|
|
175
242
|
user.memberOf = arr
|
|
176
243
|
} else {
|
|
177
|
-
const
|
|
178
|
-
if (!
|
|
179
|
-
user.memberOf = [
|
|
244
|
+
const id = await dnToSidGuid(baseEntity, user.memberOf)
|
|
245
|
+
if (!id) throw new Error(`dnToGuid did not return any objectGUID value for dn=${user.memberOf}`)
|
|
246
|
+
user.memberOf = [id]
|
|
180
247
|
}
|
|
181
248
|
} catch (err) {
|
|
182
249
|
throw new Error(err.message)
|
|
@@ -258,8 +325,14 @@ scimgateway.deleteUser = async (baseEntity, id) => {
|
|
|
258
325
|
|
|
259
326
|
const method = 'del'
|
|
260
327
|
let base
|
|
261
|
-
if (config.
|
|
262
|
-
|
|
328
|
+
if (config.useSID_id) {
|
|
329
|
+
const sid = convertStringToSid(id)
|
|
330
|
+
if (!sid) throw new Error(`${action} error: id=${id} - attribute having a none valid SID string`)
|
|
331
|
+
base = `<SID=${sid}>`
|
|
332
|
+
} else if (config.useGUID_id) {
|
|
333
|
+
const guid = Buffer.from(id, 'base64').toString('hex')
|
|
334
|
+
base = `<GUID=${guid}>`
|
|
335
|
+
} else base = id // dn
|
|
263
336
|
const ldapOptions = {}
|
|
264
337
|
|
|
265
338
|
try {
|
|
@@ -300,8 +373,14 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
|
|
|
300
373
|
|
|
301
374
|
const method = 'modify'
|
|
302
375
|
let base
|
|
303
|
-
if (config.
|
|
304
|
-
|
|
376
|
+
if (config.useSID_id) {
|
|
377
|
+
const sid = convertStringToSid(id)
|
|
378
|
+
if (!sid) throw new Error(`${action} error: id=${id} - attribute having a none valid SID string`)
|
|
379
|
+
base = `<SID=${sid}>`
|
|
380
|
+
} else if (config.useGUID_id) {
|
|
381
|
+
const guid = Buffer.from(id, 'base64').toString('hex')
|
|
382
|
+
base = `<GUID=${guid}>`
|
|
383
|
+
} else base = id // dn
|
|
305
384
|
|
|
306
385
|
try {
|
|
307
386
|
if (body.add[groupsAttr].length > 0) {
|
|
@@ -336,8 +415,14 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
|
|
|
336
415
|
const activeAttr = 'userAccountControl'
|
|
337
416
|
const method = 'search'
|
|
338
417
|
let base
|
|
339
|
-
if (config.
|
|
340
|
-
|
|
418
|
+
if (config.useSID_id) {
|
|
419
|
+
const sid = convertStringToSid(id)
|
|
420
|
+
if (!sid) throw new Error(`${action} error: id=${id} - attribute having a none valid SID string`)
|
|
421
|
+
base = `<SID=${sid}>`
|
|
422
|
+
} else if (config.useGUID_id) {
|
|
423
|
+
const guid = Buffer.from(id, 'base64').toString('hex')
|
|
424
|
+
base = `<GUID=${guid}>`
|
|
425
|
+
} else base = id // dn
|
|
341
426
|
const ldapOptions = {
|
|
342
427
|
attributes: activeAttr
|
|
343
428
|
}
|
|
@@ -366,8 +451,14 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
|
|
|
366
451
|
|
|
367
452
|
const method = 'modify'
|
|
368
453
|
let base
|
|
369
|
-
if (config.
|
|
370
|
-
|
|
454
|
+
if (config.useSID_id) {
|
|
455
|
+
const sid = convertStringToSid(id)
|
|
456
|
+
if (!sid) throw new Error(`${action} error: id=${id} - attribute having a none valid SID string`)
|
|
457
|
+
base = `<SID=${sid}>`
|
|
458
|
+
} else if (config.useGUID_id) {
|
|
459
|
+
const guid = Buffer.from(id, 'base64').toString('hex')
|
|
460
|
+
base = `<GUID=${guid}>`
|
|
461
|
+
} else base = id // dn
|
|
371
462
|
const ldapOptions = {
|
|
372
463
|
operation: 'replace',
|
|
373
464
|
modification: endpointObj
|
|
@@ -433,19 +524,40 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
|
|
|
433
524
|
if (getObj.operator) {
|
|
434
525
|
if (getObj.operator === 'eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
|
|
435
526
|
// mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
|
|
436
|
-
if (getObj.attribute === 'id') { // using dn or objectGUID (Active Directory)
|
|
437
|
-
if (config.
|
|
438
|
-
|
|
527
|
+
if (getObj.attribute === 'id') { // lookup using dn or objectSid/objectGUID (Active Directory)
|
|
528
|
+
if (config.useSID_id) {
|
|
529
|
+
const sid = convertStringToSid(getObj.value) // sid using formatted string instead of default hex
|
|
530
|
+
if (!sid) throw new Error(`${action} error: ${getObj.attribute}=${getObj.value} - attribute having a none valid SID string`)
|
|
531
|
+
base = `<SID=${sid}>`
|
|
532
|
+
} else if (config.useGUID_id) {
|
|
533
|
+
const guid = Buffer.from(getObj.value, 'base64').toString('hex')
|
|
534
|
+
base = `<GUID=${guid}>`
|
|
535
|
+
} else base = getObj.value
|
|
439
536
|
ldapOptions = {
|
|
440
537
|
attributes: attrs
|
|
441
538
|
}
|
|
442
539
|
} else {
|
|
443
540
|
const [groupIdAttr, err] = scimgateway.endpointMapper('outbound', getObj.attribute, config.map.group)
|
|
444
541
|
if (err) throw new Error(`${action} error: ${err.message}`)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
542
|
+
if (groupIdAttr === 'objectSid') {
|
|
543
|
+
const sid = convertStringToSid(getObj.value)
|
|
544
|
+
if (!sid) throw new Error(`${action} error: ${getObj.attribute}=${getObj.value} - attribute having a none valid SID string`)
|
|
545
|
+
base = `<SID=${sid}>`
|
|
546
|
+
ldapOptions = {
|
|
547
|
+
attributes: attrs
|
|
548
|
+
}
|
|
549
|
+
} else if (groupIdAttr === 'objectGUID') {
|
|
550
|
+
const guid = Buffer.from(getObj.value, 'base64').toString('hex')
|
|
551
|
+
base = `<GUID=${guid}>`
|
|
552
|
+
ldapOptions = {
|
|
553
|
+
attributes: attrs
|
|
554
|
+
}
|
|
555
|
+
} else { // search instead of lookup
|
|
556
|
+
ldapOptions = {
|
|
557
|
+
filter: `&${getObjClassFilter(baseEntity, 'group')}(${groupIdAttr}=${getObj.value})`, // &(objectClass=group)(cn=Group1)
|
|
558
|
+
scope: scope,
|
|
559
|
+
attributes: attrs
|
|
560
|
+
}
|
|
449
561
|
}
|
|
450
562
|
}
|
|
451
563
|
} else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
|
|
@@ -476,20 +588,20 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
|
|
|
476
588
|
else {
|
|
477
589
|
const groups = await doRequest(baseEntity, method, base, ldapOptions)
|
|
478
590
|
result.Resources = await Promise.all(groups.map(async (group) => { // Promise.all because of async map
|
|
479
|
-
if (config.
|
|
591
|
+
if (config.useSID_id || config.useGUID_id) {
|
|
480
592
|
if (group.member) {
|
|
481
593
|
const arr = []
|
|
482
594
|
if (Array.isArray(group.member)) {
|
|
483
595
|
for (let i = 0; i < group.member.length; i++) {
|
|
484
|
-
const
|
|
485
|
-
if (!
|
|
486
|
-
arr.push(
|
|
596
|
+
const id = await dnToSidGuid(baseEntity, group.member[i])
|
|
597
|
+
if (!id) throw new Error(`dnToSidGuid() did not return any ${config.useSID_id ? 'objectSid' : 'objectGUID'} value for dn=${group.member[i]}`)
|
|
598
|
+
arr.push(id)
|
|
487
599
|
}
|
|
488
600
|
group.member = arr
|
|
489
601
|
} else {
|
|
490
|
-
const
|
|
491
|
-
if (!
|
|
492
|
-
group.member = [
|
|
602
|
+
const id = await dnToSidGuid(baseEntity, group.member)
|
|
603
|
+
if (!id) throw new Error(`dnToSidGuid() did not return any ${config.useSID_id ? 'objectSid' : 'objectGUID'} value for ${group.member}`)
|
|
604
|
+
group.member = [id]
|
|
493
605
|
}
|
|
494
606
|
}
|
|
495
607
|
}
|
|
@@ -544,8 +656,14 @@ scimgateway.deleteGroup = async (baseEntity, id) => {
|
|
|
544
656
|
if (!config.map.group) throw new Error(`${action} error: missing configuration endpoint.map.group`)
|
|
545
657
|
const method = 'del'
|
|
546
658
|
let base
|
|
547
|
-
if (config.
|
|
548
|
-
|
|
659
|
+
if (config.useSID_id) {
|
|
660
|
+
const sid = convertStringToSid(id)
|
|
661
|
+
if (!sid) throw new Error(`${action} error: id=${id} - attribute having a none valid SID string`)
|
|
662
|
+
base = `<SID=${sid}>`
|
|
663
|
+
} else if (config.useGUID_id) {
|
|
664
|
+
const guid = Buffer.from(id, 'base64').toString('hex')
|
|
665
|
+
base = `<GUID=${guid}>`
|
|
666
|
+
} else base = id // dn
|
|
549
667
|
const ldapOptions = {}
|
|
550
668
|
|
|
551
669
|
try {
|
|
@@ -580,9 +698,9 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj) => {
|
|
|
580
698
|
|
|
581
699
|
for (let i = 0; i < attrObj.members.length; i++) {
|
|
582
700
|
const el = attrObj.members[i]
|
|
583
|
-
if (config.
|
|
584
|
-
const dn = await
|
|
585
|
-
if (!dn) throw new Error(`${action} error:
|
|
701
|
+
if (config.useSID_id || config.useGUID_id) {
|
|
702
|
+
const dn = await sidGuidToDn(baseEntity, el.value)
|
|
703
|
+
if (!dn) throw new Error(`${action} error: sidGuidToDn() did not return any objectGUID value for dn=${el.value}`)
|
|
586
704
|
el.value = dn
|
|
587
705
|
}
|
|
588
706
|
if (el.operation && el.operation === 'delete') { // delete member from group
|
|
@@ -594,7 +712,8 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj) => {
|
|
|
594
712
|
|
|
595
713
|
const method = 'modify'
|
|
596
714
|
let base
|
|
597
|
-
if (config.
|
|
715
|
+
if (config.useSID_id) base = `<SID=${id}>`
|
|
716
|
+
else if (config.useGUID_id) base = `<GUID=${id}>`
|
|
598
717
|
else base = id // dn
|
|
599
718
|
|
|
600
719
|
try {
|
|
@@ -643,20 +762,23 @@ const getObjClassFilter = (baseEntity, type) => {
|
|
|
643
762
|
}
|
|
644
763
|
|
|
645
764
|
//
|
|
646
|
-
//
|
|
765
|
+
// dnToSidGuid is used for Active Directory to return objectGUID based on dn
|
|
647
766
|
//
|
|
648
|
-
const
|
|
767
|
+
const dnToSidGuid = async (baseEntity, dn) => {
|
|
649
768
|
const method = 'search'
|
|
650
|
-
const ldapOptions = {
|
|
651
|
-
|
|
652
|
-
|
|
769
|
+
const ldapOptions = {}
|
|
770
|
+
if (config.useSID_id) ldapOptions.attributes = ['objectSid']
|
|
771
|
+
else if (config.useGUID_id) ldapOptions.attributes = ['objectGUID']
|
|
772
|
+
else throw new Error('dnToSidGuid() invalid call, configuration not using objectSid or objectGUID')
|
|
773
|
+
|
|
653
774
|
try {
|
|
654
775
|
const base = dn
|
|
655
776
|
const objects = await doRequest(baseEntity, method, base, ldapOptions)
|
|
656
777
|
if (objects.length !== 1) throw new Error(`did not find unique object having dn=${base}`)
|
|
657
|
-
return objects[0].
|
|
778
|
+
if (config.useSID_id) return objects[0].objectSid
|
|
779
|
+
else return objects[0].objectGUID
|
|
658
780
|
} catch (err) {
|
|
659
|
-
const newErr = err
|
|
781
|
+
const newErr = new Error(`dnToSidGuid() ${err.message}`)
|
|
660
782
|
throw newErr
|
|
661
783
|
}
|
|
662
784
|
}
|
|
@@ -664,41 +786,127 @@ const dnToGuid = async (baseEntity, dn) => {
|
|
|
664
786
|
//
|
|
665
787
|
// guidToDn is used for Active Directory to return dn based on objectGUID
|
|
666
788
|
//
|
|
667
|
-
const
|
|
789
|
+
const sidGuidToDn = async (baseEntity, id) => {
|
|
668
790
|
const method = 'search'
|
|
669
791
|
const ldapOptions = {
|
|
670
792
|
attributes: ['dn']
|
|
671
793
|
}
|
|
672
794
|
try {
|
|
673
|
-
|
|
795
|
+
let base
|
|
796
|
+
if (config.useSID_id) {
|
|
797
|
+
const sid = convertStringToSid(id)
|
|
798
|
+
if (!sid) throw new Error(`sidGuidToDn() error: id=${id} - attribute having a none valid SID string`)
|
|
799
|
+
base = `<SID=${sid}>`
|
|
800
|
+
} else if (config.useGUID_id) {
|
|
801
|
+
const guid = Buffer.from(id, 'base64').toString('hex')
|
|
802
|
+
base = `<GUID=${guid}>`
|
|
803
|
+
} else throw new Error('invalid call to sidGuidToDn(), configuration not using objectSid or objectGUID')
|
|
674
804
|
const objects = await doRequest(baseEntity, method, base, ldapOptions)
|
|
675
|
-
if (objects.length !== 1) throw new Error(`did not find unique object having objectGUID=${
|
|
805
|
+
if (objects.length !== 1) throw new Error(`did not find unique object having ${config.useSID_id ? 'objectSid' : 'objectGUID'} =${id}`)
|
|
676
806
|
return objects[0].dn
|
|
677
807
|
} catch (err) {
|
|
678
|
-
const newErr = err
|
|
808
|
+
const newErr = new Error(`sidGuidToDN() ${err.message}`)
|
|
679
809
|
throw newErr
|
|
680
810
|
}
|
|
681
811
|
}
|
|
682
812
|
|
|
813
|
+
//
|
|
814
|
+
// convertSidToString converts hex encoded object SID to a string
|
|
815
|
+
// e.g.
|
|
816
|
+
// input: 0105000000000005150000002ec85f9ed78d59fa176c9e9c7a040000
|
|
817
|
+
// output: S-1-5-21-2657077294-4200173015-2627628055-1146
|
|
818
|
+
// ref: https://gist.github.com/Krizzzn/0ae47f280cca9749c67759a9adedc015
|
|
819
|
+
//
|
|
820
|
+
const pad = function (s) { if (s.length < 2) { return `0${s}` } else { return s } }
|
|
821
|
+
const convertSidToString = (buf) => {
|
|
822
|
+
let asc, end
|
|
823
|
+
let i
|
|
824
|
+
if (buf == null) { return null }
|
|
825
|
+
const version = buf[0]
|
|
826
|
+
const subAuthorityCount = buf[1]
|
|
827
|
+
const identifierAuthority = parseInt(((() => {
|
|
828
|
+
const result = []
|
|
829
|
+
for (i = 2; i <= 7; i++) {
|
|
830
|
+
result.push(buf[i].toString(16))
|
|
831
|
+
}
|
|
832
|
+
return result
|
|
833
|
+
})()).join(''), 16)
|
|
834
|
+
let sidString = `S-${version}-${identifierAuthority}`
|
|
835
|
+
try {
|
|
836
|
+
for (i = 0, end = subAuthorityCount - 1, asc = end >= 0; asc ? i <= end : i >= end; asc ? i++ : i--) {
|
|
837
|
+
const subAuthOffset = i * 4
|
|
838
|
+
const tmp =
|
|
839
|
+
pad(buf[11 + subAuthOffset].toString(16)) +
|
|
840
|
+
pad(buf[10 + subAuthOffset].toString(16)) +
|
|
841
|
+
pad(buf[9 + subAuthOffset].toString(16)) +
|
|
842
|
+
pad(buf[8 + subAuthOffset].toString(16))
|
|
843
|
+
sidString += `-${parseInt(tmp, 16)}`
|
|
844
|
+
}
|
|
845
|
+
} catch (err) {
|
|
846
|
+
return null
|
|
847
|
+
}
|
|
848
|
+
return sidString
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
//
|
|
852
|
+
// convertStringToSid converts SID string to hex encoded object SID
|
|
853
|
+
// e.g.
|
|
854
|
+
// input: S-1-5-21-2127521184-1604012920-1887927527-72713
|
|
855
|
+
// output: 010500000000000515000000a065cf7e784b9b5fe77c8770091c0100
|
|
856
|
+
// ref: https://devblogs.microsoft.com/oldnewthing/20040315-00/?p=40253
|
|
857
|
+
//
|
|
858
|
+
const convertStringToSid = (sidStr) => {
|
|
859
|
+
const arr = sidStr.split('-')
|
|
860
|
+
if (arr.length !== 8) return null
|
|
861
|
+
try {
|
|
862
|
+
const b0 = 0x0100000000000000n // S-1 = 01
|
|
863
|
+
const b1 = 0x0005000000000000n // seven dashes, seven minus two = 5
|
|
864
|
+
const b2 = BigInt(arr[2]) // 0x5n
|
|
865
|
+
const b02 = b0 | b1 | b2 // big-endian
|
|
866
|
+
const bufBE = Buffer.alloc(8)
|
|
867
|
+
bufBE.writeBigUInt64BE(b02, 0)
|
|
868
|
+
let res = bufBE.toString('hex') // 0105000000000005
|
|
869
|
+
// rest is little-endian
|
|
870
|
+
const bufLE = Buffer.alloc(4)
|
|
871
|
+
for (let i = 3; i < arr.length; i++) {
|
|
872
|
+
const val = parseInt(arr[i], 10 >>> 0) // int32 to unsigned int
|
|
873
|
+
bufLE.writeUInt32LE(val.toString(), 0)
|
|
874
|
+
res += bufLE.toString('hex')
|
|
875
|
+
}
|
|
876
|
+
return res
|
|
877
|
+
} catch (err) {
|
|
878
|
+
return null
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
683
882
|
//
|
|
684
883
|
// getMemberOfGroups returns all groups the user is member of
|
|
685
884
|
// [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
|
|
686
885
|
//
|
|
687
886
|
const getMemberOfGroups = async (baseEntity, id) => {
|
|
688
887
|
const action = 'getMemberOfGroups'
|
|
689
|
-
|
|
690
888
|
if (!config.map.group) throw new Error('missing configuration endpoint.map.group') // not using groups
|
|
691
889
|
|
|
692
890
|
let idDn = id
|
|
693
|
-
if (config.
|
|
891
|
+
if (config.useSID_id || config.useGUID_id) { // need dn
|
|
694
892
|
const method = 'search'
|
|
695
|
-
|
|
893
|
+
let base
|
|
894
|
+
if (config.useSID_id) {
|
|
895
|
+
const sid = convertStringToSid(id)
|
|
896
|
+
if (!sid) throw new Error(`${action} error: ${id}=${id} - attribute having a none valid SID string`)
|
|
897
|
+
base = `<SID=${sid}>`
|
|
898
|
+
} else {
|
|
899
|
+
const guid = Buffer.from(id, 'base64').toString('hex')
|
|
900
|
+
base = `<GUID=${guid}>`
|
|
901
|
+
}
|
|
902
|
+
|
|
696
903
|
const ldapOptions = {
|
|
697
904
|
attributes: ['dn']
|
|
698
905
|
}
|
|
906
|
+
|
|
699
907
|
try {
|
|
700
908
|
const users = await doRequest(baseEntity, method, base, ldapOptions)
|
|
701
|
-
if (users.length !== 1) throw new Error(`${action} error: did not find unique user having objectGUID=${id}`)
|
|
909
|
+
if (users.length !== 1) throw new Error(`${action} error: did not find unique user having ${config.useSID_id ? 'objectSid' : 'objectGUID'} =${id}`)
|
|
702
910
|
idDn = users[0].dn
|
|
703
911
|
} catch (err) {
|
|
704
912
|
const newErr = err
|
|
@@ -748,8 +956,6 @@ const getServiceClient = async (baseEntity) => {
|
|
|
748
956
|
if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
|
|
749
957
|
|
|
750
958
|
for (let i = -1; i < config.entity[baseEntity].baseUrls.length; i++) {
|
|
751
|
-
let useStrictDN = true
|
|
752
|
-
if (config.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') useStrictDN = false
|
|
753
959
|
try {
|
|
754
960
|
const cli = await ldap.createClient({
|
|
755
961
|
url: config.entity[baseEntity].baseUrl,
|
|
@@ -757,24 +963,25 @@ const getServiceClient = async (baseEntity) => {
|
|
|
757
963
|
tlsOptions: {
|
|
758
964
|
rejectUnauthorized: false
|
|
759
965
|
},
|
|
760
|
-
strictDN:
|
|
966
|
+
strictDN: false // false => allows none standard ldap base dn e.g. <SID=...> / <GUID=...> ref. objectSid/objectGUID
|
|
761
967
|
})
|
|
762
|
-
|
|
763
968
|
await new Promise((resolve, reject) => {
|
|
764
|
-
cli.bind(config.entity[baseEntity].username, config.entity[baseEntity].passwordDecrypted, (err) => err ? reject(err) : resolve())
|
|
969
|
+
cli.bind(config.entity[baseEntity].username, config.entity[baseEntity].passwordDecrypted, (err, res) => err ? reject(err) : resolve(res))
|
|
970
|
+
cli.on('error', (err) => reject(err))
|
|
765
971
|
})
|
|
766
972
|
return cli // client OK
|
|
767
973
|
} catch (err) {
|
|
768
|
-
|
|
769
|
-
|
|
974
|
+
const retry = err.message.includes('timeout') || err.message.includes('ECONNREFUSED')
|
|
975
|
+
if (retry && i + 1 < config.entity[baseEntity].baseUrls.length) { // failover logic
|
|
976
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] baseUrl=${config.entity[baseEntity].baseUrl} connection error - starting retry`)
|
|
770
977
|
config.entity[baseEntity].baseUrl = config.entity[baseEntity].baseUrls[i + 1]
|
|
771
978
|
} else {
|
|
772
|
-
|
|
773
|
-
throw
|
|
979
|
+
if (err.message.includes('AcceptSecurityContext')) err.message = 'LdapErr: connect failure, invalid user/password'
|
|
980
|
+
throw err
|
|
774
981
|
}
|
|
775
982
|
}
|
|
776
983
|
}
|
|
777
|
-
throw new Error(
|
|
984
|
+
throw new Error(`${action} logic failed for some odd reasons - should not happend...`)
|
|
778
985
|
}
|
|
779
986
|
|
|
780
987
|
//
|
|
@@ -792,7 +999,16 @@ const getServiceClient = async (baseEntity) => {
|
|
|
792
999
|
const doRequest = async (baseEntity, method, base, ldapOptions) => {
|
|
793
1000
|
let result = null
|
|
794
1001
|
let client = null
|
|
1002
|
+
|
|
795
1003
|
const options = scimgateway.copyObj(ldapOptions)
|
|
1004
|
+
if (options.filter) {
|
|
1005
|
+
if (options.filter && options.filter.includes('userPrincipalName') && config.upnMapDomain) {
|
|
1006
|
+
const old = options.filter
|
|
1007
|
+
options.filter = options.filter.replace(config.upnMapDomain.inbound, config.upnMapDomain.outbound)
|
|
1008
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] inbound upnMapDomain ${old} => ${options.filter}`)
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
796
1012
|
try {
|
|
797
1013
|
client = await getServiceClient(baseEntity)
|
|
798
1014
|
switch (method) {
|
|
@@ -807,9 +1023,20 @@ const doRequest = async (baseEntity, method, base, ldapOptions) => {
|
|
|
807
1023
|
search.on('searchEntry', (entry) => {
|
|
808
1024
|
if (entry.attributes) {
|
|
809
1025
|
entry.attributes.find((el, i) => {
|
|
810
|
-
if (
|
|
811
|
-
const
|
|
812
|
-
|
|
1026
|
+
if (['objectSid', 'objectGUID'].includes(el.type)) { // assume Active Directory - can't use default utf-8 when attribute value is hex
|
|
1027
|
+
const b = Buffer.from(el.buffers[0], 'hex')
|
|
1028
|
+
if (el.type === 'objectSid') {
|
|
1029
|
+
const sidStr = convertSidToString(b) // using string: S-1-5-21-2657077294-4200173015-2627628055-1255
|
|
1030
|
+
if (!sidStr) throw new Error(`doRequest() error: failed to convert SID ${b.toString('hex')} to string}`)
|
|
1031
|
+
entry.attributes[i]._vals = [sidStr]
|
|
1032
|
+
} else {
|
|
1033
|
+
entry.attributes[i]._vals = [b.toString('base64')] // using base64: nitWLrhokUqKl1DywiavXg==
|
|
1034
|
+
}
|
|
1035
|
+
} else if (el.type === 'userPrincipalName' && config.upnMapDomain) {
|
|
1036
|
+
const val = Buffer.from(el.buffers[0], 'hex').toString('utf8')
|
|
1037
|
+
const old = val
|
|
1038
|
+
entry.attributes[i]._vals = [val.replace(config.upnMapDomain.outbound, config.upnMapDomain.inbound)]
|
|
1039
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] outbound upnMapDomain ${old} => ${entry.attributes[i]._vals}`)
|
|
813
1040
|
}
|
|
814
1041
|
return undefined
|
|
815
1042
|
})
|
|
@@ -820,8 +1047,11 @@ const doRequest = async (baseEntity, method, base, ldapOptions) => {
|
|
|
820
1047
|
search.on('page', (entry, cb) => {
|
|
821
1048
|
// if (cb) cb() // pagePause = true gives callback
|
|
822
1049
|
})
|
|
823
|
-
search.on('error', (err) =>
|
|
824
|
-
|
|
1050
|
+
search.on('error', (err) => {
|
|
1051
|
+
if (err.message.includes('LdapErr: DSID-0C0909F2') || err.message.includes('NO_OBJECT')) return resolve([]) // object not found when using base <SID=...> or <GUID=...> ref. objectSid/objectGUID
|
|
1052
|
+
reject(err)
|
|
1053
|
+
})
|
|
1054
|
+
search.on('end', (_) => { resolve(results) })
|
|
825
1055
|
})
|
|
826
1056
|
})
|
|
827
1057
|
break
|
|
@@ -831,6 +1061,9 @@ const doRequest = async (baseEntity, method, base, ldapOptions) => {
|
|
|
831
1061
|
const dn = base
|
|
832
1062
|
client.modify(dn, options, (err) => {
|
|
833
1063
|
if (err) {
|
|
1064
|
+
if (options.operation && options.operation === 'add' && options.modification && options.modification.member) {
|
|
1065
|
+
if (err.message.includes('ENTRY_EXISTS')) return resolve() // add already existing group to user
|
|
1066
|
+
}
|
|
834
1067
|
return reject(err)
|
|
835
1068
|
}
|
|
836
1069
|
resolve()
|
|
@@ -866,11 +1099,10 @@ const doRequest = async (baseEntity, method, base, ldapOptions) => {
|
|
|
866
1099
|
client.unbind()
|
|
867
1100
|
} catch (err) {
|
|
868
1101
|
scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest method=${method} base=${base} ldapOptions=${JSON.stringify(ldapOptions)} Error Response = ${err.message}`)
|
|
869
|
-
const newErr = err
|
|
870
1102
|
if (client) {
|
|
871
|
-
try { client.
|
|
1103
|
+
try { client.destroy() } catch (err) {}
|
|
872
1104
|
}
|
|
873
|
-
throw
|
|
1105
|
+
throw err
|
|
874
1106
|
}
|
|
875
1107
|
|
|
876
1108
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest method=${method} base=${base} ldapOptions=${JSON.stringify(ldapOptions)} Response=${JSON.stringify(result)}`)
|
package/lib/plugin-loki.js
CHANGED
|
@@ -215,7 +215,10 @@ scimgateway.createUser = async (baseEntity, userObj) => {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
userObj.id = userObj.userName //
|
|
218
|
+
if (userObj.userName) userObj.id = userObj.userName // id set to userName or externalId
|
|
219
|
+
else if (userObj.externalId) userObj.id = userObj.externalId
|
|
220
|
+
else throw new Error(`${action} error: missing mandatory userName or externalId`)
|
|
221
|
+
|
|
219
222
|
try {
|
|
220
223
|
users.insert(userObj)
|
|
221
224
|
return null
|
|
@@ -415,10 +418,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
|
|
|
415
418
|
|
|
416
419
|
if (!groupsArr) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
|
417
420
|
|
|
418
|
-
if (!getObj.startIndex
|
|
419
|
-
|
|
420
|
-
getObj.count = groupsArr.length
|
|
421
|
-
}
|
|
421
|
+
if (!getObj.startIndex) getObj.startIndex = 1
|
|
422
|
+
if (!getObj.count) getObj.count = 200
|
|
422
423
|
|
|
423
424
|
const ret = {
|
|
424
425
|
Resources: [],
|
|
@@ -439,7 +440,8 @@ scimgateway.createGroup = async (baseEntity, groupObj) => {
|
|
|
439
440
|
const action = 'createGroup'
|
|
440
441
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
|
|
441
442
|
|
|
442
|
-
groupObj.id = groupObj.
|
|
443
|
+
if (groupObj.externalId) groupObj.id = groupObj.externalId // for loki-plugin (scim endpoint) id is mandatory and set to displayName
|
|
444
|
+
else groupObj.id = groupObj.displayName
|
|
443
445
|
|
|
444
446
|
try {
|
|
445
447
|
groups.insert(groupObj)
|
package/lib/plugin-mongodb.js
CHANGED
|
@@ -266,7 +266,9 @@ scimgateway.createUser = async (baseEntity, userObj) => {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
userObj.id = userObj.userName //
|
|
269
|
+
if (userObj.userName) userObj.id = userObj.userName // id set to userName or externalId
|
|
270
|
+
else if (userObj.externalId) userObj.id = userObj.externalId
|
|
271
|
+
else throw new Error(`${action} error: missing mandatory userName or externalId`)
|
|
270
272
|
|
|
271
273
|
if (!userObj.meta) {
|
|
272
274
|
const now = Date.now()
|
|
@@ -516,10 +518,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
|
|
|
516
518
|
|
|
517
519
|
if (!findObj) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
|
518
520
|
|
|
519
|
-
if (!getObj.startIndex
|
|
520
|
-
|
|
521
|
-
getObj.count = 200
|
|
522
|
-
}
|
|
521
|
+
if (!getObj.startIndex) getObj.startIndex = 1
|
|
522
|
+
if (!getObj.count) getObj.count = 200
|
|
523
523
|
|
|
524
524
|
const ret = {
|
|
525
525
|
Resources: [],
|
|
@@ -559,7 +559,8 @@ scimgateway.createGroup = async (baseEntity, groupObj) => {
|
|
|
559
559
|
lastModified: now
|
|
560
560
|
}
|
|
561
561
|
}
|
|
562
|
-
groupObj.id = groupObj.
|
|
562
|
+
if (groupObj.externalId) groupObj.id = groupObj.externalId // for loki-plugin (scim endpoint) id is mandatory and set to displayName
|
|
563
|
+
else groupObj.id = groupObj.displayName
|
|
563
564
|
groupObj = encodeDotDate(groupObj)
|
|
564
565
|
|
|
565
566
|
try {
|
|
File without changes
|
package/lib/postinstall.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
//
|
|
2
2
|
// Copy plugins from original scimgateway package to current installation folder
|
|
3
3
|
//
|
|
4
|
-
var fs = require('fs')
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
|
|
7
|
+
function fsExistsSync (f) {
|
|
7
8
|
try {
|
|
8
9
|
fs.accessSync(f)
|
|
9
10
|
return true
|
|
10
11
|
} catch (e) {
|
|
11
|
-
|
|
12
|
+
return false
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
if (
|
|
16
|
+
if (process.env.npm_config_scimgateway_postinstall_skip || process.env.SCIMGATEWAY_POSTINSTALL_SKIP) {
|
|
17
|
+
console.info('The configuration `scimgateway_postinstall_skip` was set to true so `postinstall` activities are going to be skipped!')
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (fsExistsSync('./node_modules')) process.exit(0) // global package - quit - no postinstall
|
|
16
22
|
|
|
17
23
|
if (!fsExistsSync('../../config')) fs.mkdirSync('../../config')
|
|
18
24
|
if (!fsExistsSync('../../config/certs')) fs.mkdirSync('../../config/certs')
|
|
@@ -22,7 +28,7 @@ if (!fsExistsSync('../../config/docker')) fs.mkdirSync('../../config/docker')
|
|
|
22
28
|
if (!fsExistsSync('../../lib')) fs.mkdirSync('../../lib')
|
|
23
29
|
|
|
24
30
|
if (!fsExistsSync('../../config/plugin-loki.json')) fs.writeFileSync('../../config/plugin-loki.json', fs.readFileSync('./config/plugin-loki.json'))
|
|
25
|
-
if (!fsExistsSync('../../config/plugin-
|
|
31
|
+
if (!fsExistsSync('../../config/plugin-scim.json')) fs.writeFileSync('../../config/plugin-scim.json', fs.readFileSync('./config/plugin-scim.json'))
|
|
26
32
|
if (!fsExistsSync('../../config/plugin-forwardinc.json')) fs.writeFileSync('../../config/plugin-forwardinc.json', fs.readFileSync('./config/plugin-forwardinc.json'))
|
|
27
33
|
if (!fsExistsSync('../../config/plugin-mssql.json')) fs.writeFileSync('../../config/plugin-mssql.json', fs.readFileSync('./config/plugin-mssql.json'))
|
|
28
34
|
if (!fsExistsSync('../../config/plugin-saphana.json')) fs.writeFileSync('../../config/plugin-saphana.json', fs.readFileSync('./config/plugin-saphana.json'))
|
|
@@ -32,7 +38,7 @@ if (!fsExistsSync('../../config/plugin-ldap.json')) fs.writeFileSync('../../conf
|
|
|
32
38
|
if (!fsExistsSync('../../config/plugin-mongodb.json')) fs.writeFileSync('../../config/plugin-mongodb.json', fs.readFileSync('./config/plugin-mongodb.json'))
|
|
33
39
|
|
|
34
40
|
fs.writeFileSync('../../lib/plugin-loki.js', fs.readFileSync('./lib/plugin-loki.js'))
|
|
35
|
-
fs.writeFileSync('../../lib/plugin-
|
|
41
|
+
fs.writeFileSync('../../lib/plugin-scim.js', fs.readFileSync('./lib/plugin-scim.js'))
|
|
36
42
|
fs.writeFileSync('../../lib/plugin-forwardinc.js', fs.readFileSync('./lib/plugin-forwardinc.js'))
|
|
37
43
|
fs.writeFileSync('../../lib/plugin-mssql.js', fs.readFileSync('./lib/plugin-mssql.js'))
|
|
38
44
|
fs.writeFileSync('../../lib/plugin-saphana.js', fs.readFileSync('./lib/plugin-saphana.js'))
|
|
@@ -41,10 +47,12 @@ fs.writeFileSync('../../lib/plugin-azure-ad.js', fs.readFileSync('./lib/plugin-a
|
|
|
41
47
|
fs.writeFileSync('../../lib/plugin-ldap.js', fs.readFileSync('./lib/plugin-ldap.js'))
|
|
42
48
|
fs.writeFileSync('../../lib/plugin-mongodb.js', fs.readFileSync('./lib/plugin-mongodb.js'))
|
|
43
49
|
|
|
44
|
-
if (!fsExistsSync('../../config/wsdls/GroupService.wsdl'))
|
|
50
|
+
if (!fsExistsSync('../../config/wsdls/GroupService.wsdl')) {
|
|
45
51
|
fs.writeFileSync('../../config/wsdls/GroupService.wsdl', fs.readFileSync('./config/wsdls/GroupService.wsdl'))
|
|
46
|
-
|
|
52
|
+
}
|
|
53
|
+
if (!fsExistsSync('../../config/wsdls/UserService.wsdl')) {
|
|
47
54
|
fs.writeFileSync('../../config/wsdls/UserService.wsdl', fs.readFileSync('./config/wsdls/UserService.wsdl'))
|
|
55
|
+
}
|
|
48
56
|
|
|
49
57
|
fs.writeFileSync('../../config/docker/docker-compose.yml', fs.readFileSync('./config/docker/docker-compose.yml'))
|
|
50
58
|
fs.writeFileSync('../../config/docker/Dockerfile', fs.readFileSync('./config/docker/Dockerfile'))
|
package/lib/scimgateway.js
CHANGED
|
@@ -422,7 +422,7 @@ const ScimGateway = function () {
|
|
|
422
422
|
const arr = ctx.request.url.split('/')
|
|
423
423
|
if (arr.length > 0) {
|
|
424
424
|
const entity = arr[1].split('?')[0]
|
|
425
|
-
if (!['Users', 'Groups', 'scim'].includes(entity)) baseEntity = entity
|
|
425
|
+
if (!['Users', 'Groups', 'Schemas', 'ServiceProviderConfigs', 'scim'].includes(entity)) baseEntity = entity
|
|
426
426
|
}
|
|
427
427
|
try { // authenticate
|
|
428
428
|
const arrResolve = await Promise.all([unauth(ctx), basic(baseEntity, ctx.request.method, authType, authToken), bearerToken(baseEntity, ctx.request.method, authType, authToken), bearerJwtAzure(baseEntity, ctx, next, authType, authToken), bearerJwt(baseEntity, ctx.request.method, authType, authToken)]).catch((err) => { throw (err) })
|
|
@@ -777,7 +777,7 @@ const ScimGateway = function () {
|
|
|
777
777
|
// GET /Groups
|
|
778
778
|
// GET /Users?attributes=userName&startIndex=1&count=100
|
|
779
779
|
// GET /Groups?attributes=displayName
|
|
780
|
-
// GET /Users?filter=meta.created
|
|
780
|
+
// GET /Users?filter=meta.created ge "2010-01-01T00:00:00Z"&attributes=userName,id,name.familyName,meta.created
|
|
781
781
|
// GET /Users?filter=emails.value co "@example.com"&attributes=userName,name.familyName,emails&sortBy=name.familyName&sortOrder=descending
|
|
782
782
|
|
|
783
783
|
let info = ''
|
|
@@ -816,7 +816,7 @@ const ScimGateway = function () {
|
|
|
816
816
|
logger.debug(`${gwName}[${pluginName}] calling "${handler.groups.getMethod}" and awaiting result - groups to be included`)
|
|
817
817
|
let res
|
|
818
818
|
try {
|
|
819
|
-
res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: userObj.id }, ['members.value', 'id', 'displayName']) // await scimgateway.getUserGroups(baseEntity, userObj.id, 'members.value,displayName')
|
|
819
|
+
res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(userObj.id) }, ['members.value', 'id', 'displayName']) // await scimgateway.getUserGroups(baseEntity, userObj.id, 'members.value,displayName')
|
|
820
820
|
} catch (err) {} // method may be implemented but throwing error like groups not supported/implemented
|
|
821
821
|
if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
|
|
822
822
|
userObj.groups = []
|
|
@@ -913,18 +913,27 @@ const ScimGateway = function () {
|
|
|
913
913
|
jsonBody[key] = res[key]
|
|
914
914
|
}
|
|
915
915
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (
|
|
916
|
+
let obj
|
|
917
|
+
try { // retrieve id
|
|
918
|
+
if (handle.createMethod === 'createUser') {
|
|
919
|
+
if (jsonBody.userName) {
|
|
920
|
+
jsonBody.id = jsonBody.userName
|
|
920
921
|
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'userName', operator: 'eq', value: jsonBody.userName }, ['id', 'userName'])
|
|
921
|
-
} else if (
|
|
922
|
+
} else if (jsonBody.externalId) {
|
|
923
|
+
jsonBody.id = jsonBody.externalId
|
|
924
|
+
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, ['id', 'externalId'])
|
|
925
|
+
}
|
|
926
|
+
} else if (handle.createMethod === 'createGroup') {
|
|
927
|
+
if (jsonBody.externalId) {
|
|
928
|
+
jsonBody.id = jsonBody.externalId
|
|
929
|
+
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, ['id', 'externalId'])
|
|
930
|
+
} else if (jsonBody.displayName) {
|
|
931
|
+
jsonBody.id = jsonBody.displayName
|
|
922
932
|
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'displayName', operator: 'eq', value: jsonBody.displayName }, ['id', 'displayName'])
|
|
923
933
|
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
}
|
|
934
|
+
}
|
|
935
|
+
} catch (err) { }
|
|
936
|
+
if (obj && obj.id) jsonBody.id = obj.id
|
|
928
937
|
|
|
929
938
|
const location = `${ctx.origin}${ctx.path}/${jsonBody.id}`
|
|
930
939
|
if (!jsonBody.meta) jsonBody.meta = {}
|
|
@@ -1663,9 +1672,10 @@ const getMultivalueTypes = (objName, scimDef) => { // objName = 'User' or 'Group
|
|
|
1663
1672
|
ScimGateway.prototype.processExtConfig = function processExtConfig (pluginName, config, isMain) {
|
|
1664
1673
|
const processEnv = 'process.env.'
|
|
1665
1674
|
const processFile = 'process.file.'
|
|
1675
|
+
const processText = 'process.text.'
|
|
1666
1676
|
const dotConfig = dot.dot(config)
|
|
1667
|
-
|
|
1668
|
-
|
|
1677
|
+
const processTexts = new Map()
|
|
1678
|
+
const processFiles = new Map()
|
|
1669
1679
|
|
|
1670
1680
|
for (const key in dotConfig) {
|
|
1671
1681
|
let value = dotConfig[key]
|
|
@@ -1678,15 +1688,26 @@ ScimGateway.prototype.processExtConfig = function processExtConfig (pluginName,
|
|
|
1678
1688
|
newErr.name = 'processExtConfig'
|
|
1679
1689
|
throw newErr
|
|
1680
1690
|
}
|
|
1691
|
+
} else if (value && value.constructor === String && value.includes(processText)) {
|
|
1692
|
+
const filePath = value.substring(processText.length)
|
|
1693
|
+
try {
|
|
1694
|
+
if (!processTexts.has(filePath)) { // avoid reading previous file
|
|
1695
|
+
processTexts.set(filePath, fs.readFileSync(filePath, 'utf8'))
|
|
1696
|
+
}
|
|
1697
|
+
value = processTexts.get(filePath) // directly a string
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
value = undefined
|
|
1700
|
+
throw new Error(`configuration failed - can't read text from external file: "${filePath}"`)
|
|
1701
|
+
}
|
|
1702
|
+
dotConfig[key] = value
|
|
1681
1703
|
} else if (value && value.constructor === String && value.includes(processFile)) {
|
|
1682
|
-
const
|
|
1704
|
+
const filePath = value.substring(processFile.length)
|
|
1683
1705
|
try {
|
|
1684
|
-
if (filePath
|
|
1685
|
-
filePath
|
|
1686
|
-
content = fs.readFileSync(filePath, 'utf8')
|
|
1706
|
+
if (!processFiles.has(filePath)) { // avoid reading previous file
|
|
1707
|
+
processFiles.set(filePath, JSON.parse(fs.readFileSync(filePath, 'utf8')))
|
|
1687
1708
|
}
|
|
1688
1709
|
try {
|
|
1689
|
-
const jContent =
|
|
1710
|
+
const jContent = processFiles.get(filePath) // json or json-dot-notation formatting is supported
|
|
1690
1711
|
const dotContent = dot.dot(dot.object(jContent))
|
|
1691
1712
|
let newKey = null
|
|
1692
1713
|
if (isMain) newKey = `${pluginName}.scimgateway.${key}`
|
|
@@ -1721,7 +1742,8 @@ ScimGateway.prototype.processExtConfig = function processExtConfig (pluginName,
|
|
|
1721
1742
|
dotConfig[key] = value
|
|
1722
1743
|
}
|
|
1723
1744
|
}
|
|
1724
|
-
|
|
1745
|
+
processTexts.clear()
|
|
1746
|
+
processFiles.clear()
|
|
1725
1747
|
return dot.object(dotConfig)
|
|
1726
1748
|
}
|
|
1727
1749
|
|
package/lib/utils.js
CHANGED
|
@@ -69,11 +69,17 @@ module.exports.getPassword = function (pwDotNotation, configFile) {
|
|
|
69
69
|
if (pw.includes('process.')) { // password based on external reference e.g. environment or json file
|
|
70
70
|
// syntax environment = "process.env.<ENVIRONMENT>" e.g. scimgateway.password could have value "process.env.PORT", then using environment variable PORT
|
|
71
71
|
// syntax file = "process.file.<PATH>" e.g. scimgateway.password could have value "process.file./tmp/myconf.json"
|
|
72
|
+
const processText = 'process.text.'
|
|
72
73
|
const processEnv = 'process.env.'
|
|
73
74
|
const processFile = 'process.file.'
|
|
74
75
|
if (pw.constructor === String && pw.includes(processEnv)) {
|
|
75
76
|
const envKey = pw.substring(processEnv.length)
|
|
76
77
|
pwclear = process.env[envKey]
|
|
78
|
+
} else if (pw && pw.constructor === String && pw.includes(processText)) {
|
|
79
|
+
const filePath = pw.substring(processFile.length)
|
|
80
|
+
try {
|
|
81
|
+
pwclear = fs.readFileSync(filePath, 'utf8')
|
|
82
|
+
} catch (err) { pwclear = undefined } // can't read external configuration file
|
|
77
83
|
} else if (pw && pw.constructor === String && pw.includes(processFile)) {
|
|
78
84
|
const filePath = pw.substring(processFile.length)
|
|
79
85
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scimgateway",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
|
|
5
5
|
"author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
|
|
6
6
|
"homepage": "https://elshaug.xyz",
|
|
@@ -45,15 +45,15 @@
|
|
|
45
45
|
"mongodb": "^4.2.0",
|
|
46
46
|
"node-machine-id": "1.1.9",
|
|
47
47
|
"nodemailer": "^6.7.1",
|
|
48
|
-
"passport": "^0.5.
|
|
48
|
+
"passport": "^0.5.2",
|
|
49
49
|
"passport-azure-ad": "^4.3.1",
|
|
50
50
|
"soap": "^0.43.0",
|
|
51
51
|
"tedious": "^14.0.0",
|
|
52
|
-
"winston": "^3.
|
|
52
|
+
"winston": "^3.4.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"chai": "^4.2.0",
|
|
56
|
-
"mocha": "^
|
|
56
|
+
"mocha": "^9.2.0",
|
|
57
57
|
"supertest": "^4.0.2"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/test/index.js
CHANGED
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
//
|
|
11
11
|
|
|
12
12
|
const loki = require('./lib/plugin-loki')
|
|
13
|
-
const
|
|
13
|
+
const scim = require('./lib/plugin-scim')
|
|
14
14
|
const api = require('./lib/plugin-api')
|
|
15
|
-
// const mongodb = require('./lib/plugin-mongodb')
|
|
15
|
+
// const mongodb = require('./lib/plugin-mongodb')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const expect = require('chai').expect
|
|
4
|
-
const scimgateway = require('../../lib/plugin-
|
|
4
|
+
const scimgateway = require('../../lib/plugin-scim.js')
|
|
5
5
|
const server_8886 = require('supertest').agent('http://localhost:8886') // module request is an alternative
|
|
6
6
|
|
|
7
7
|
const auth = 'Basic ' + Buffer.from('gwadmin:password').toString('base64')
|
|
@@ -12,7 +12,7 @@ var options = {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
describe('plugin-
|
|
15
|
+
describe('plugin-scim tests', () => {
|
|
16
16
|
|
|
17
17
|
it('getUsers all test (1)', function (done) {
|
|
18
18
|
server_8886.get('/Users' +
|