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 CHANGED
@@ -1,7 +1,7 @@
1
1
  language: node_js
2
2
 
3
3
  node_js:
4
- - "8"
4
+ - "10"
5
5
 
6
6
  sudo: false
7
7
 
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
  ![](https://jelhub.github.io/images/ScimGateway.svg)
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
- * **RESTful** (REST Webservice)
60
- Demonstrates user provisioning towards REST-Based endpoint
61
- Using plugin "Loki" as a REST endpoint
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 user provisioning towards SOAP-Based endpoint
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 gte "2010-01-01T00:00:00Z"&attributes=userName,name.familyName,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 restful = require('./lib/plugin-restful')
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
@@ -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 specific attributes like userAccountControl, unicodePW
9
- // and objectGUID. objectGUID can also be used as id instead of dn
10
- // by replacing config.map.user.dn with config.map.user.objectGUID
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) lookup instead of search
114
- if (config.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') base = `<GUID=${getObj.value}>` // '<GUID=b3975b675d3a21498b4e511e1a8ccb9e>'
115
- else base = getObj.value
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
- ldapOptions = {
123
- filter: `&${getObjClassFilter(baseEntity, 'user')}(${userIdAttr}=${getObj.value})`, // &(objectClass=user)(objectClass=person)(objectClass=organizationalPerson)(objectClass=top)(sAMAccountName=bjensen)
124
- scope: scope,
125
- attributes: attrs
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.map.group.objectGUID && config.map.group.objectGUID.mapTo === 'id') { // Active Directory using objectGUID - convert memberOf having dn values to objectGUID
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 guid = await dnToGuid(baseEntity, user.memberOf[i])
172
- if (!guid) throw new Error(`dnToGuid did not return any objectGUID value for dn=${user.memberOf[i]}`)
173
- arr.push(guid)
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 guid = await dnToGuid(baseEntity, user.memberOf)
178
- if (!guid) throw new Error(`dnToGuid did not return any objectGUID value for dn=${user.memberOf}`)
179
- user.memberOf = [guid]
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.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') base = `<GUID=${id}>` // AD objectGUID
262
- else base = id // dn
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.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') base = `<GUID=${id}>` // AD objectGUID
304
- else base = id // dn
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.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') base = `<GUID=${id}>` // AD objectGUID
340
- else base = id // dn
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.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') base = `<GUID=${id}>` // AD objectGUID
370
- else base = id // dn
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) lookup instead of search
437
- if (config.map.group.objectGUID && config.map.group.objectGUID.mapTo === 'id') base = `<GUID=${getObj.value}>` // '<GUID=b3975b675d3a21498b4e511e1a8ccb9e>'
438
- else base = getObj.value
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
- ldapOptions = {
446
- filter: `&${getObjClassFilter(baseEntity, 'group')}(${groupIdAttr}=${getObj.value})`, // &(objectClass=group)(cn=Group1)
447
- scope: scope,
448
- attributes: attrs
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.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') {
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 guid = await dnToGuid(baseEntity, group.member[i])
485
- if (!guid) throw new Error(`dnToGuid did not return any objectGUID value for dn=${group.member[i]}`)
486
- arr.push(guid)
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 guid = await dnToGuid(baseEntity, group.member)
491
- if (!guid) throw new Error(`dnToGuid did not return any objectGUID value for dn=${group.member}`)
492
- group.member = [guid]
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.map.group.objectGUID && config.map.group.objectGUID.mapTo === 'id') base = `<GUID=${id}>` // AD objectGUID
548
- else base = id // dn
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.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') {
584
- const dn = await guidToDn(baseEntity, el.value)
585
- if (!dn) throw new Error(`${action} error: dnToGuid did not return any objectGUID value for dn=${el.value}`)
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.map.group.objectGUID && config.map.group.objectGUID.mapTo === 'id') base = `<GUID=${id}>` // AD objectGUID
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
- // dnToGuid is used for Active Directory to return objectGUID based on dn
765
+ // dnToSidGuid is used for Active Directory to return objectGUID based on dn
647
766
  //
648
- const dnToGuid = async (baseEntity, dn) => {
767
+ const dnToSidGuid = async (baseEntity, dn) => {
649
768
  const method = 'search'
650
- const ldapOptions = {
651
- attributes: ['objectGUID']
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].objectGUID
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 guidToDn = async (baseEntity, guid) => {
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
- const base = `<GUID=${guid}>`
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=${base}`)
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.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id') { // need dn
891
+ if (config.useSID_id || config.useGUID_id) { // need dn
694
892
  const method = 'search'
695
- const base = `<GUID=${id}>`
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: useStrictDN // false => supports objectGUID as dn
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
- if (i + 1 < config.entity[baseEntity].baseUrls.length) { // failover logic
769
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] baseUrl=${config.entity[baseEntity].baseUrl} connection error but will retry`)
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
- const newErr = err
773
- throw newErr
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('getServiceClient program logic failed for some odd reasons - should not happend...')
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 (el.type === 'objectGUID') { // assume Active Directory - can't use default utf-8 when attribute value is hex
811
- const hexStr = Buffer.from(el.buffers[0], 'binary').toString('hex')
812
- entry.attributes[i]._vals = [hexStr]
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) => reject(err))
824
- search.on('end', (_) => resolve(results))
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.unbind() } catch (err) {}
1103
+ try { client.destroy() } catch (err) {}
872
1104
  }
873
- throw newErr
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)}`)
@@ -215,7 +215,10 @@ scimgateway.createUser = async (baseEntity, userObj) => {
215
215
  }
216
216
  }
217
217
 
218
- userObj.id = userObj.userName // for loki-plugin (scim endpoint) id is mandatory and set to 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 && !getObj.count) { // client request without paging
419
- getObj.startIndex = 1
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.displayName // for loki-plugin (scim endpoint) id is mandatory and set to displayName
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)
@@ -266,7 +266,9 @@ scimgateway.createUser = async (baseEntity, userObj) => {
266
266
  }
267
267
  }
268
268
 
269
- userObj.id = userObj.userName // for loki-plugin (scim endpoint) id is mandatory and set to 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 && !getObj.count) { // client request without paging
520
- getObj.startIndex = 1
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.displayName // for loki-plugin (scim endpoint) id is mandatory and set to displayName
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
@@ -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
- function fsExistsSync(f) {
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
- return false
12
+ return false
12
13
  }
13
14
  }
14
15
 
15
- if (fsExistsSync('./node_modules')) return true // global package - quit - no postinstall
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-restful.json')) fs.writeFileSync('../../config/plugin-restful.json', fs.readFileSync('./config/plugin-restful.json'))
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-restful.js', fs.readFileSync('./lib/plugin-restful.js'))
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
- if (!fsExistsSync('../../config/wsdls/UserService.wsdl'))
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'))
@@ -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 gte "2010-01-01T00:00:00Z"&attributes=userName,id,name.familyName,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
- if (!jsonBody.id) { // retrieve id
917
- let obj
918
- try {
919
- if (handle.getMethod === 'getUser') {
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 (handle.getMethod === 'getGroup') {
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
- } catch (err) { }
925
- if (obj && obj.id) jsonBody.id = obj.id
926
- else jsonBody.id = jsonBody.userName ? jsonBody.userName : jsonBody.displayName
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
- let content
1668
- let filePath
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 newFilePath = value.substring(processFile.length)
1704
+ const filePath = value.substring(processFile.length)
1683
1705
  try {
1684
- if (filePath !== newFilePath) { // avoid reading previous file
1685
- filePath = newFilePath
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 = JSON.parse(content) // json or json-dot-notation formatting is supported
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
- content = null
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.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.0",
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.3.3"
52
+ "winston": "^3.4.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "chai": "^4.2.0",
56
- "mocha": "^7.1.1",
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 restful = require('./lib/plugin-restful')
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-restful.js')
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-restful tests', () => {
15
+ describe('plugin-scim tests', () => {
16
16
 
17
17
  it('getUsers all test (1)', function (done) {
18
18
  server_8886.get('/Users' +