scimgateway 4.5.7 → 4.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Validated through IdP's:
16
16
 
17
17
  Latest news:
18
18
 
19
- - Supports stream publishing mode having [SCIM Stream](https://elshaug.xyz/docs/scim-stream) as a prerequisite. In this mode, standard incoming SCIM requests from your Identity Provider (IdP) or API are directed and published to the stream. Subsequently, one of the gateways subscribing to the channel utilized by the publisher will manage the SCIM request, and response back. Using SCIM Stream we have egress/outbound only traffic and get loadbalancing/failover by adding more gateways subscribing to the same channel.
19
+ - Supports stream publishing mode having [SCIM Stream](https://elshaug.xyz/docs/scim-stream) as a prerequisite. In this mode, standard incoming SCIM requests from your Identity Provider (IdP) or API are directed and published to the stream. Subsequently, one of the gateways subscribing to the channel utilized by the publisher will manage the SCIM request, and response back. Using SCIM Stream we have only egress/outbound traffic and get loadbalancing/failover by adding more gateways subscribing to the same channel.
20
20
  - **BREAKING**: [SCIM Stream](https://elshaug.xyz/docs/scim-stream) is the modern way of user provisioning letting clients subscribe to messages instead of traditional IGA top-down provisioning. SCIM Gateway now offers enhanced functionality with support for message subscription and automated provisioning using SCIM Stream
21
21
  - Authentication PassThrough letting plugin pass authentication directly to endpoint for avoid maintaining secrets at the gateway. Kubernetes health checks and shutdown handler support
22
22
  - Supports OAuth Client Credentials authentication
@@ -1163,6 +1163,48 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1163
1163
 
1164
1164
  ## Change log
1165
1165
 
1166
+ ### v4.5.8
1167
+
1168
+ [Fixed]
1169
+
1170
+ - plugin-ldap failed when using national special characters and some other LDAP special characters in DN
1171
+
1172
+ Note, plugin-ldap now has following new configuration:
1173
+
1174
+ "ldap": {
1175
+ "isOpenLdap": false,
1176
+ ...
1177
+ "namingAttribute": {
1178
+ "user": [
1179
+ {
1180
+ "attribute": "CN",
1181
+ "mapTo": "userName"
1182
+ }
1183
+ ],
1184
+ "group": [
1185
+ {
1186
+ "attribute": "CN",
1187
+ "mapTo": "displayName"
1188
+ }
1189
+ ]
1190
+ },
1191
+ ...
1192
+ }
1193
+
1194
+ `isOpenLdap` true/false decides whether or not OpenLDAP Foundation protocol should be used for national characters and special characters in DN. For Active Directory, default isOpenLdap=false should be used.
1195
+
1196
+ `namingAttribute` can now be linked to scim `mapTo` attribute and is not hardcoded like it was in previous version.
1197
+
1198
+ Previous `userNamingAttr` and `groupNamingAttr` shown below, is now deprecated
1199
+
1200
+ "ldap": {
1201
+ ...
1202
+ "userNamingAttr": "CN",
1203
+ "groupNamingAttr": "CN",
1204
+ ...
1205
+ }
1206
+
1207
+
1166
1208
  ### v4.5.7
1167
1209
 
1168
1210
  [Fixed]
@@ -139,12 +139,25 @@
139
139
  "username": "CN=Administrator,CN=Users,DC=test,DC=com",
140
140
  "password": "password",
141
141
  "ldap": {
142
+ "isOpenLdap": false,
142
143
  "userBase": "CN=Users,DC=test,DC=com",
143
144
  "groupBase": "OU=Groups,DC=test,DC=com",
144
145
  "userFilter": null,
145
146
  "groupFilter": null,
146
- "userNamingAttr": "CN",
147
- "groupNamingAttr": "CN",
147
+ "namingAttribute": {
148
+ "user": [
149
+ {
150
+ "attribute": "CN",
151
+ "mapTo": "userName"
152
+ }
153
+ ],
154
+ "group": [
155
+ {
156
+ "attribute": "CN",
157
+ "mapTo": "displayName"
158
+ }
159
+ ]
160
+ },
148
161
  "userObjectClasses": [
149
162
  "user",
150
163
  "person",
@@ -31,6 +31,10 @@
31
31
  // ...
32
32
  // }
33
33
  //
34
+ // Configuration isOpenLdap true/false decides whether or not OpenLDAP Foundation protocol should
35
+ // be used for national characters and special characters in DN.
36
+ // For Active Directory, default isOpenLdap=false should be used
37
+ //
34
38
  // Attributes according to map definition in the configuration file plugin-ldap.json:
35
39
  //
36
40
  // GlobalUser Template Scim Endpoint
@@ -70,6 +74,7 @@
70
74
  'use strict'
71
75
 
72
76
  const ldap = require('ldapjs')
77
+ const { BerReader } = require('@ldapjs/asn1')
73
78
 
74
79
  // start - mandatory plugin initialization
75
80
  let ScimGateway = null
@@ -86,33 +91,6 @@ config = scimgateway.processExtConfig(pluginName, config) // add any external co
86
91
  scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no scimgateway authentication). scimgateway instead includes ctx (ctx.request.header) in plugin methods. Note, requires plugin-logic for handling/passing ctx.request.header.authorization to be used in endpoint communication
87
92
  // end - mandatory plugin initialization
88
93
 
89
- if (!config.map || !config.map.user) {
90
- scimgateway.logger.error(`${pluginName} map.user configuration is mandatory`)
91
- process.exit(1)
92
- }
93
-
94
- config.useSID_id = config.map.user.objectSid && config.map.user.objectSid.mapTo === 'id' // AD proprietary SID/GUID
95
- config.useGUID_id = config.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id'
96
- if (config.useSID_id && config.map.group) {
97
- if (!config.map.group.objectSid || config.map.group.objectSid.mapTo !== 'id') {
98
- scimgateway.logger.error(`${pluginName} missing configuration group.objectSid - user and group should be using the same attribute`)
99
- process.exit(1)
100
- }
101
- } else if (config.useGUID_id && config.map.group) {
102
- if (!config.map.group.objectGUID || config.map.group.objectGUID.mapTo !== 'id') {
103
- scimgateway.logger.error(`${pluginName} missing configuration group.objectGUID - user and group should be using the same attribute`)
104
- process.exit(1)
105
- }
106
- }
107
- if (config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) { // support mapping different inbound/outbound upn domain names
108
- if (config.map.user.userPrincipalName.mapDomain.inbound && config.map.user.userPrincipalName.mapDomain.outbound) {
109
- const inbound = config.map.user.userPrincipalName.mapDomain.inbound
110
- const outbound = config.map.user.userPrincipalName.mapDomain.outbound
111
- config.map.user.userPrincipalName.mapDomain.inbound = inbound.startsWith('@') ? inbound : '@' + inbound // "@my-company.com"
112
- config.map.user.userPrincipalName.mapDomain.outbound = outbound.startsWith('@') ? outbound : '@' + outbound // "@test.onmicrosoft.com
113
- } else delete config.map.user.userPrincipalName.mapDomain
114
- }
115
-
116
94
  // =================================================
117
95
  // getUsers
118
96
  // =================================================
@@ -185,12 +163,12 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
185
163
  attributes: attrs
186
164
  }
187
165
  } else { // search instead of lookup
166
+ const filter = createAndFilter(baseEntity, 'user', [{ attribute: userIdAttr, value: getObj.value }])
188
167
  ldapOptions = {
189
- filter: `&${getObjClassFilter(baseEntity, 'user')}(${userIdAttr}=${getObj.value})`, // &(objectClass=user)(objectClass=person)(objectClass=organizationalPerson)(objectClass=top)(sAMAccountName=bjensen)
190
- scope: scope,
168
+ filter,
169
+ scope: 'sub',
191
170
  attributes: attrs
192
171
  }
193
- if (config.entity[baseEntity].ldap.userFilter) ldapOptions.filter += config.entity[baseEntity].ldap.userFilter
194
172
  }
195
173
  }
196
174
  } else if (getObj.operator === 'eq' && getObj.attribute === 'group.value') {
@@ -199,12 +177,14 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
199
177
  } else {
200
178
  // optional - simpel filtering
201
179
  if (getObj.operator === 'eq') {
180
+ const [filterAttr, err] = scimgateway.endpointMapper('outbound', getObj.attribute, config.map.user)
181
+ if (err) throw new Error(`${action} error: ${err.message}`)
182
+ const filter = createAndFilter(baseEntity, 'user', [{ attribute: filterAttr, value: getObj.value }])
202
183
  ldapOptions = {
203
- filter: `&${getObjClassFilter(baseEntity, 'user')}(${getObj.attribute}=${getObj.value})`, // &(objectClass=user)(objectClass=person)(objectClass=organizationalPerson)(objectClass=top)(employeeNumber=123)
204
- scope: scope,
184
+ filter,
185
+ scope,
205
186
  attributes: attrs
206
187
  }
207
- if (config.entity[baseEntity].ldap.userFilter) ldapOptions.filter += config.entity[baseEntity].ldap.userFilter
208
188
  } else {
209
189
  throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
210
190
  }
@@ -214,12 +194,12 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
214
194
  throw new Error(`${action} error: not supporting advanced filtering: ${getObj.rawFilter}`)
215
195
  } else {
216
196
  // mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all users to be returned - correspond to exploreUsers() in versions < 4.x.x
197
+ const filter = createAndFilter(baseEntity, 'user', [{ attribute: userIdAttr, value: '*' }])
217
198
  ldapOptions = {
218
- filter: `&${getObjClassFilter(baseEntity, 'user')}(${userIdAttr}=*)`,
219
- scope: scope,
199
+ filter,
200
+ scope,
220
201
  attributes: attrs
221
202
  }
222
- if (config.entity[baseEntity].ldap.userFilter) ldapOptions.filter += config.entity[baseEntity].ldap.userFilter
223
203
  }
224
204
  // end mandatory if-else logic
225
205
 
@@ -263,7 +243,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
263
243
  }
264
244
 
265
245
  const scimObj = scimgateway.endpointMapper('inbound', user, config.map.user)[0] // endpoint attribute naming => SCIM
266
- if (!scimObj.groups) scimObj.groups = []
246
+ // if (!scimObj.groups) scimObj.groups = []
267
247
  return scimObj
268
248
  }))
269
249
  } catch (err) {
@@ -317,8 +297,16 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
317
297
  // endpointObj.objectClass is mandatory and must must match your ldap schema
318
298
  endpointObj.objectClass = config.entity[baseEntity].ldap.userObjectClasses // Active Directory: ["user", "person", "organizationalPerson", "top"]
319
299
 
300
+ let base = ''
301
+ const [userNamingAttr, scimAttr] = getNamingAttribute(baseEntity, 'user') // ['CN', 'userName']
302
+ const arr = scimAttr.split('.')
303
+ if (arr.length < 2) {
304
+ base = `${userNamingAttr}=${userObj[scimAttr]},${userBase}`
305
+ } else {
306
+ base = `${userNamingAttr}=${userObj[arr[0]][arr[1]]},${userBase}`
307
+ }
308
+
320
309
  const method = 'add'
321
- const base = `${config.entity[baseEntity].ldap.userNamingAttr}=${userObj.userName},${userBase}`
322
310
  const ldapOptions = endpointObj
323
311
 
324
312
  try {
@@ -371,7 +359,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
371
359
  delete attrObj.groups // make sure to be removed from attrObj
372
360
 
373
361
  const [groupsAttr] = scimgateway.endpointMapper('outbound', 'groups.value', config.map.user)
374
- const grp = { add: { }, remove: { } }
362
+ const grp = { add: {}, remove: {} }
375
363
  grp.add[groupsAttr] = []
376
364
  grp.remove[groupsAttr] = []
377
365
 
@@ -510,8 +498,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
510
498
  totalResults: null
511
499
  }
512
500
 
513
- if (!config.map.group || !config.entity[baseEntity].ldap.groupBase) { // not using groups
514
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] "${action}" stopped - missing configuration endpoint.map.group or groupBase`)
501
+ if (!config?.map?.group || !config.entity[baseEntity]?.ldap?.groupBase) { // not using groups
502
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] "${action}" skip group handling - missing configuration endpoint.map.group or groupBase`)
515
503
  return result
516
504
  }
517
505
 
@@ -536,7 +524,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
536
524
  // mandatory if-else logic - start
537
525
  if (getObj.operator) {
538
526
  if (getObj.operator === 'eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
539
- // mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
527
+ // mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
540
528
  if (getObj.attribute === 'id') { // lookup using dn or objectSid/objectGUID (Active Directory)
541
529
  if (config.useSID_id) {
542
530
  const sid = convertStringToSid(getObj.value) // sid using formatted string instead of default hex
@@ -566,12 +554,12 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
566
554
  attributes: attrs
567
555
  }
568
556
  } else { // search instead of lookup
557
+ const filter = createAndFilter(baseEntity, 'group', [{ attribute: groupIdAttr, value: getObj.value }])
569
558
  ldapOptions = {
570
- filter: `&${getObjClassFilter(baseEntity, 'group')}(${groupIdAttr}=${getObj.value})`, // &(objectClass=group)(cn=Group1)
571
- scope: scope,
559
+ filter,
560
+ scope,
572
561
  attributes: attrs
573
562
  }
574
- if (config.entity[baseEntity].ldap.groupFilter) ldapOptions.filter += config.entity[baseEntity].ldap.groupFilter
575
563
  }
576
564
  }
577
565
  } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
@@ -580,19 +568,30 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
580
568
  ldapOptions = 'getMemberOfGroups'
581
569
  } else {
582
570
  // optional - simpel filtering
583
- throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
571
+ if (getObj.operator === 'eq') {
572
+ const [filterAttr, err] = scimgateway.endpointMapper('outbound', getObj.attribute, config.map.group)
573
+ if (err) throw new Error(`${action} error: ${err.message}`)
574
+ const filter = createAndFilter(baseEntity, 'group', [{ attribute: filterAttr, value: getObj.value }])
575
+ ldapOptions = {
576
+ filter,
577
+ scope,
578
+ attributes: attrs
579
+ }
580
+ } else {
581
+ throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
582
+ }
584
583
  }
585
584
  } else if (getObj.rawFilter) {
586
585
  // optional - advanced filtering having and/or/not - use getObj.rawFilter
587
586
  throw new Error(`${action} error: not supporting advanced filtering: ${getObj.rawFilter}`)
588
587
  } else {
589
- // mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all groups to be returned - correspond to exploreGroups() in versions < 4.x.x
588
+ // mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all groups to be returned - correspond to exploreGroups() in versions < 4.x.x
589
+ const filter = createAndFilter(baseEntity, 'group', [{ attribute: groupDisplayNameAttr, value: '*' }])
590
590
  ldapOptions = {
591
- filter: `&${getObjClassFilter(baseEntity, 'group')}(${groupDisplayNameAttr}=*)`,
592
- scope: scope,
591
+ filter,
592
+ scope,
593
593
  attributes: attrs
594
594
  }
595
- if (config.entity[baseEntity].ldap.groupFilter) ldapOptions.filter += config.entity[baseEntity].ldap.groupFilter
596
595
  }
597
596
  // mandatory if-else logic - end
598
597
 
@@ -639,6 +638,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
639
638
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
640
639
 
641
640
  if (!config.map.group) throw new Error(`${action} error: missing configuration endpoint.map.group`)
641
+ const groupBase = config.entity[baseEntity].ldap.groupBase
642
642
 
643
643
  // convert SCIM attributes to endpoint attributes according to config.map
644
644
  const [endpointObj] = scimgateway.endpointMapper('outbound', groupObj, config.map.group)
@@ -646,13 +646,23 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
646
646
  // endpointObj.objectClass is mandatory and must must match your ldap schema
647
647
  endpointObj.objectClass = config.entity[baseEntity].ldap.groupObjectClasses // Active Directory: ["group"]
648
648
 
649
+ let base = ''
650
+ const [groupNamingAttr, scimAttr] = getNamingAttribute(baseEntity, 'group') // ['CN', 'displayName']
651
+ const arr = scimAttr.split('.')
652
+ if (arr.length < 2) {
653
+ base = `${groupNamingAttr}=${groupObj[scimAttr]},${groupBase}`
654
+ } else {
655
+ base = `${groupNamingAttr}=${groupObj[arr[0]][arr[1]]},${groupBase}`
656
+ }
657
+
649
658
  const method = 'add'
650
- const base = `${config.entity[baseEntity].ldap.groupNamingAttr}=${groupObj.displayName},${config.entity[baseEntity].ldap.groupBase}`
651
659
  const ldapOptions = endpointObj
652
660
 
653
661
  try {
654
662
  await doRequest(baseEntity, method, base, ldapOptions, ctx)
655
- return null
663
+ const res = await scimgateway.getGroups(baseEntity, { attribute: 'id', operator: 'eq', value: base }, [], ctx)
664
+ if (res && Array.isArray(res.Resources) && res.Resources.length === 1) return res.Resources[0]
665
+ else return null
656
666
  } catch (err) {
657
667
  const newErr = new Error(`${action} error: ${err.message}`)
658
668
  if (newErr.message.includes('ENTRY_EXISTS')) newErr.name += '#409' // customErrCode
@@ -703,7 +713,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
703
713
  const [memberAttr] = scimgateway.endpointMapper('outbound', 'members.value', config.map.group)
704
714
  if (!memberAttr && attrObj.members) throw new Error(`${action} error: missing attribute mapping configuration for group members`)
705
715
 
706
- const grp = { add: { }, remove: { } }
716
+ const grp = { add: {}, remove: {} }
707
717
  grp.add[memberAttr] = []
708
718
  grp.remove[memberAttr] = []
709
719
 
@@ -764,22 +774,82 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
764
774
  const _serviceClient = {}
765
775
 
766
776
  //
767
- // getObjClassFilter returns object classes to be included in search
777
+ // createAndFilter creates AndFilter object to be used as filter instead of standard string filter
778
+ // Using AndFilter object for eliminating internal ldapjs escaping problems related to values with some
779
+ // combinations of parentheses e.g. ab(c)d
768
780
  //
769
- const getObjClassFilter = (baseEntity, type) => {
770
- let filter = ''
781
+ const createAndFilter = (baseEntity, type, arrObj) => {
782
+ const objFilters = []
783
+
784
+ // add arrObj
785
+ for (let i = 0; i < arrObj.length; i++) {
786
+ if (arrObj[i].value.indexOf('*') > -1) { // SubstringFilter or PresenceFilter
787
+ const arr = arrObj[i].value.split('*')
788
+ if (arr.length === 2 && !arr[0] && !arr[1]) { // cn=*
789
+ const f = new ldap.PresenceFilter({ attribute: arrObj[i].attribute })
790
+ objFilters.push(f)
791
+ } else { // cn=ab*cd*e
792
+ const fObj = {
793
+ attribute: arrObj[i].attribute
794
+ }
795
+ const arrAny = []
796
+ fObj.initial = arr[0]
797
+ if (!fObj.initial) delete fObj.initial
798
+ for (let i = 1; i < arr.length - 1; i++) {
799
+ arrAny.push(arr[i])
800
+ }
801
+ fObj.any = arrAny
802
+ if (arr[arr.length - 1]) {
803
+ fObj.final = arr[arr.length - 1]
804
+ }
805
+ const f = new ldap.SubstringFilter(fObj)
806
+ objFilters.push(f)
807
+ }
808
+ } else { // EqualityFilter cn=abc
809
+ const f = new ldap.EqualityFilter({ attribute: arrObj[i].attribute, value: arrObj[i].value })
810
+ objFilters.push(f)
811
+ }
812
+ }
813
+
814
+ // add from configuration objectClass and userFiter/groupFilter
771
815
  switch (type) {
772
816
  case 'user':
773
817
  for (let i = 0; i < config.entity[baseEntity].ldap.userObjectClasses.length; i++) {
774
- filter += `(objectClass=${config.entity[baseEntity].ldap.userObjectClasses[i]})`
818
+ const f = new ldap.EqualityFilter({ attribute: 'objectClass', value: config.entity[baseEntity].ldap.userObjectClasses[i] })
819
+ objFilters.push(f)
820
+ }
821
+ if (config.entity[baseEntity].ldap.userFilter) {
822
+ try {
823
+ const uf = ldap.parseFilter(config.entity[baseEntity].ldap.userFilter)
824
+ objFilters.push(uf)
825
+ } catch (err) {
826
+ throw new Error(`configuration ldap.userFilter: ${config.entity[baseEntity].ldap.userFilter} - parseFilter error: ${err.message}`)
827
+ }
775
828
  }
776
829
  break
777
830
  case 'group':
778
831
  for (let i = 0; i < config.entity[baseEntity].ldap.groupObjectClasses.length; i++) {
779
- filter += `(objectClass=${config.entity[baseEntity].ldap.groupObjectClasses[i]})`
832
+ const f = new ldap.EqualityFilter({ attribute: 'objectClass', value: config.entity[baseEntity].ldap.groupObjectClasses[i] })
833
+ objFilters.push(f)
834
+ if (config.entity[baseEntity].ldap.groupFilter) {
835
+ try {
836
+ const gf = ldap.parseFilter(config.entity[baseEntity].ldap.groupFilter)
837
+ objFilters.push(gf)
838
+ } catch (err) {
839
+ throw new Error(`configuration ldap.groupFilter: ${config.entity[baseEntity].ldap.groupFilter} - parseFilter error: ${err.message}`)
840
+ }
841
+ }
780
842
  }
781
843
  break
782
844
  }
845
+
846
+ // put all into AndFilter
847
+ const filter = new ldap.AndFilter({
848
+ filters: [
849
+ ...objFilters
850
+ ]
851
+ })
852
+
783
853
  return filter
784
854
  }
785
855
 
@@ -858,10 +928,10 @@ const convertSidToString = (buf) => {
858
928
  for (i = 0, end = subAuthorityCount - 1, asc = end >= 0; asc ? i <= end : i >= end; asc ? i++ : i--) {
859
929
  const subAuthOffset = i * 4
860
930
  const tmp =
861
- pad(buf[11 + subAuthOffset].toString(16)) +
862
- pad(buf[10 + subAuthOffset].toString(16)) +
863
- pad(buf[9 + subAuthOffset].toString(16)) +
864
- pad(buf[8 + subAuthOffset].toString(16))
931
+ pad(buf[11 + subAuthOffset].toString(16)) +
932
+ pad(buf[10 + subAuthOffset].toString(16)) +
933
+ pad(buf[9 + subAuthOffset].toString(16)) +
934
+ pad(buf[8 + subAuthOffset].toString(16))
865
935
  sidString += `-${parseInt(tmp, 16)}`
866
936
  }
867
937
  } catch (err) {
@@ -946,9 +1016,10 @@ const getMemberOfGroups = async (baseEntity, id, ctx) => {
946
1016
  const scope = 'sub'
947
1017
  const base = config.entity[baseEntity].ldap.groupBase
948
1018
 
1019
+ const filter = createAndFilter(baseEntity, 'group', [{ attribute: memberAttr, value: idDn }])
949
1020
  const ldapOptions = {
950
- filter: `&${getObjClassFilter(baseEntity, 'group')}(${memberAttr}=${idDn})`,
951
- scope: scope,
1021
+ filter,
1022
+ scope,
952
1023
  attributes: attrs
953
1024
  }
954
1025
 
@@ -967,6 +1038,194 @@ const getMemberOfGroups = async (baseEntity, id, ctx) => {
967
1038
  }
968
1039
  }
969
1040
 
1041
+ //
1042
+ // ldapEscDn will escape DN according to the LDAP standard adjusted to ldapjs behavior
1043
+ // using OpenLDAP, DN must be escaped - national characters and special ldap characters
1044
+ // using Active Directory (none OpenLDAP), DN should not be escaped, but DN retrieved from AD is character escaped
1045
+ //
1046
+ const ldapEscDn = (isOpenLdap, str) => {
1047
+ if (!str) return str
1048
+
1049
+ if (!isOpenLdap && str.indexOf('\\') > 0) {
1050
+ const conv = str.replace(/\\([0-9A-Fa-f]{2})/g, (_, hex) => {
1051
+ const intAscii = parseInt(hex, 16)
1052
+ if (intAscii > 128) { // extended ascii - will be unescaped by decodeURIComponent
1053
+ return '%' + hex
1054
+ } else { // use character escape
1055
+ return '\\' + String.fromCharCode(intAscii)
1056
+ }
1057
+ })
1058
+ str = decodeURIComponent(conv)
1059
+ str = str.replace(/\\/g, '') // lower ascii may be character escaped e.g. 'cn=Kürt\, Lastname' - see below comma logic
1060
+ }
1061
+
1062
+ const arr = str.split(',')
1063
+ for (let i = 0; i < arr.length; i++) { // CN=Firstname, Lastname,OU=...
1064
+ if (!arr[i]) { // value having comma only
1065
+ if (arr[i - 1].charAt(arr[i - 1].length - 1) === '\\') {
1066
+ arr[i - 1] = arr[i - 1].substring(0, arr[i - 1].length - 1)
1067
+ }
1068
+ if (isOpenLdap) arr[i - 1] += '\\,'
1069
+ else arr[i - 1] += ','
1070
+ arr.splice(i, 1)
1071
+ i -= 1
1072
+ continue
1073
+ }
1074
+ const a = arr[i].split('=')
1075
+ if (a.length < 2 && i > 0) { // value having comma and content
1076
+ if (arr[i - 1].charAt(arr[i - 1].length - 1) === '\\') {
1077
+ arr[i - 1] = arr[i - 1].substring(0, arr[i - 1].length - 1)
1078
+ }
1079
+ if (isOpenLdap) arr[i - 1] += `\\,${ldapEsc(a[0])}`
1080
+ else arr[i - 1] += `,${a[0]}`
1081
+ arr.splice(i, 1)
1082
+ i -= 1
1083
+ continue
1084
+ } else {
1085
+ if (isOpenLdap) arr[i] = `${a[0]}=${ldapEsc(a[1])}`
1086
+ else arr[i] = `${a[0]}=${a[1]}`
1087
+ }
1088
+ if (i > 0) break // only escape logic on first, assume sub OU's are correct
1089
+ }
1090
+ if (isOpenLdap) {
1091
+ str = arr.join(',')
1092
+ return str
1093
+ }
1094
+ // Using dn object and BER encoding
1095
+ // e.g., Active Directory to avoid internal ldapjs OpenLDAP validating and string escaping logic
1096
+ const dn = new ldap.DN()
1097
+ for (let i = 0; i < arr.length; i++) {
1098
+ const a = arr[i].split('=') // cn=Kürt
1099
+ if (a.length === 2) {
1100
+ if (i === 0) {
1101
+ const ua = new Uint8Array(Buffer.from(a[1], 'utf-8'))
1102
+ const buf = Buffer.from(new Uint8Array([4, ua.length, ...ua]))
1103
+ const rdn = {}
1104
+ rdn[a[0]] = new BerReader(buf)
1105
+ dn.push(new ldap.RDN(rdn))
1106
+ // new BerReader(Buffer.from([0x04, 0x05, 0x4B, 0xc3, 0xbc, 0x72, 0x74])) // Kürt
1107
+ // the leading 04 is the tag for "octet string" and the following 05 is the length in bytes of the string.
1108
+ } else {
1109
+ const rdn = {}
1110
+ rdn[a[0]] = a[1]
1111
+ dn.push(new ldap.RDN(rdn))
1112
+ }
1113
+ } else {
1114
+ throw new Error('ldapEscDn() invalid DN: ' + str)
1115
+ }
1116
+ }
1117
+ return dn
1118
+ }
1119
+
1120
+ //
1121
+ // ldapEsc will character escape str according to OpenLDAP DN standard
1122
+ // Hex encoded escaping (extended and unicode ascii) is not included because
1123
+ // automatically handled by ldapjs when not using BER encoded DN
1124
+ //
1125
+ const ldapEsc = (str) => {
1126
+ if (!str) return str
1127
+ let newStr = ''
1128
+ for (let i = 0; i < str.length; i++) {
1129
+ let c = str[i]
1130
+ let isEsc = false
1131
+ if (i > 0 && str[i - 1] === '\\') isEsc = true
1132
+ switch (c) {
1133
+ case ',':
1134
+ if (isEsc) c = ','
1135
+ else c = '\\,'
1136
+ break
1137
+ case ';':
1138
+ if (isEsc) c = ';'
1139
+ else c = '\\;'
1140
+ break
1141
+ case '+':
1142
+ if (isEsc) c = '+'
1143
+ else c = '\\+'
1144
+ break
1145
+ case '<':
1146
+ if (isEsc) c = '<'
1147
+ else c = '\\<'
1148
+ break
1149
+ case '>':
1150
+ if (isEsc) c = '>'
1151
+ else c = '\\>'
1152
+ break
1153
+ case '=':
1154
+ if (isEsc) c = '='
1155
+ else c = '\\='
1156
+ break
1157
+ case '"':
1158
+ if (isEsc) c = '"'
1159
+ else c = '\\"'
1160
+ break
1161
+ case '(':
1162
+ if (isEsc) c = '('
1163
+ else c = '\\('
1164
+ break
1165
+ case ')':
1166
+ if (isEsc) c = ')'
1167
+ else c = '\\)'
1168
+ break
1169
+ }
1170
+ newStr += c
1171
+ }
1172
+ return newStr
1173
+ }
1174
+
1175
+ //
1176
+ // berDecodeDn decodes a BER string part of type DN
1177
+ // OU=#04057573657273,OU=abc,... => OU=users,OU=abc,...
1178
+ // only using BER on first part of dn
1179
+ // Having BER decoding for Active Directory, but not for OpenLDAP
1180
+ //
1181
+ const berDecodeDn = (dn) => {
1182
+ if (Object.prototype.toString.call(dn) !== '[object LdapDn]') return dn // OpenLDAP
1183
+ const str = dn.toString()
1184
+ if (str.indexOf('#') < 1) return str
1185
+ const arr = str.split('#')
1186
+ if (arr.length === 2) {
1187
+ const a = arr[1].split(',')
1188
+ if (a.length > 1) {
1189
+ const berStr = a[0].substring(4)
1190
+ let decoded = ''
1191
+ let c = ''
1192
+ if (berStr.length % 2 === 0) {
1193
+ for (let i = 0; i < berStr.length; i++) {
1194
+ c += berStr[i]
1195
+ if (c.length === 2) {
1196
+ const intAscii = parseInt(c, 16)
1197
+ decoded += String.fromCharCode(intAscii)
1198
+ c = ''
1199
+ }
1200
+ }
1201
+ }
1202
+ if (decoded.length > 0) {
1203
+ a.splice(0, 1) // remove element 0 from array
1204
+ return `${arr[0]}${decoded},${a.join(',')}` // OU=users,OU=abc
1205
+ }
1206
+ }
1207
+ }
1208
+ return str
1209
+ }
1210
+
1211
+ const getNamingAttribute = (baseEntity, type) => {
1212
+ let arr
1213
+ switch (type) {
1214
+ case 'user':
1215
+ arr = config.entity[baseEntity]?.ldap?.namingAttribute?.user
1216
+ break
1217
+ case 'group':
1218
+ arr = config.entity[baseEntity]?.ldap?.namingAttribute?.group
1219
+ break
1220
+ default:
1221
+ throw new Error(`getNamingAttribute error: invalid type ${type}`)
1222
+ }
1223
+ if (!Array.isArray(arr) || arr.length !== 1) throw new Error(`configuration missing namingAttribute definition for ${type}`)
1224
+ const [endpointAttr] = scimgateway.endpointMapper('outbound', arr[0].mapTo, config.map[type])
1225
+ if (!endpointAttr) throw new Error(`namingAttribute mapTo:${arr[0].mapTo} cannot be found in the map ${type} configuration`)
1226
+ return [arr[0].attribute, arr[0].mapTo]
1227
+ }
1228
+
970
1229
  //
971
1230
  // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
972
1231
  //
@@ -1034,11 +1293,11 @@ const getServiceClient = async (baseEntity, ctx) => {
1034
1293
  // "attributes": ["sAMAccountName","displayName","mail"]
1035
1294
  // }
1036
1295
  //
1037
- const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1296
+ const doRequest = async (baseEntity, method, base, options, ctx) => {
1038
1297
  let result = null
1039
1298
  let client = null
1299
+ base = ldapEscDn(config.entity[baseEntity].ldap.isOpenLdap, base)
1040
1300
 
1041
- const options = scimgateway.copyObj(ldapOptions)
1042
1301
  // support having different upn-domain on IdP and target
1043
1302
  if (options.modification && options.modification.userPrincipalName && config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) {
1044
1303
  if (options.modification.userPrincipalName.endsWith(config.map.user.userPrincipalName.mapDomain.outbound)) {
@@ -1055,7 +1314,8 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1055
1314
  options.paged = { pageSize: 200, pagePause: false } // parse entire directory calling 'page' method for each page
1056
1315
  result = await new Promise((resolve, reject) => {
1057
1316
  const results = []
1058
- client.search(base, scimgateway.copyObj(options), (err, search) => {
1317
+
1318
+ client.search(base, options, (err, search) => {
1059
1319
  if (err) {
1060
1320
  return reject(err)
1061
1321
  }
@@ -1086,6 +1346,23 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1086
1346
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] outbound upnMapDomain ${old} => ${obj.userPrincipalName}`)
1087
1347
  }
1088
1348
  }
1349
+
1350
+ if (obj.dn && obj.dn.indexOf('\\') > 0) {
1351
+ // for OpenLDAP ensure dn is not hex escaped e.g.: cn=K\c3\bcrt => cn=Kürt
1352
+ // because dn may be be used as value in standard attributes like group memberOf
1353
+ obj.dn = obj.dn.replace(/\\\\/g, '__') // temp
1354
+ let conv = obj.dn.replace(/\\([0-9A-Fa-f]{2})/g, (_, hex) => {
1355
+ const intAscii = parseInt(hex, 16)
1356
+ if (intAscii > 128) { // extended ascii - will be unescaped by decodeURIComponent
1357
+ return '%' + hex
1358
+ } else { // use character escape
1359
+ return '\\' + String.fromCharCode(intAscii)
1360
+ }
1361
+ })
1362
+ conv = conv.replace(/__/g, '\\\\')
1363
+ obj.dn = decodeURIComponent(conv)
1364
+ }
1365
+
1089
1366
  results.push(obj)
1090
1367
  })
1091
1368
 
@@ -1115,11 +1392,10 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1115
1392
  if (mod.values.length > 1) { // delete before replace to keep inbound order
1116
1393
  changes.push({
1117
1394
  operation: 'delete',
1118
- modification: {type: key, values: []}
1395
+ modification: { type: key, values: [] }
1119
1396
  })
1120
1397
  }
1121
- }
1122
- else {
1398
+ } else {
1123
1399
  if (typeof options.modification[key] === 'string') mod.values = [options.modification[key]]
1124
1400
  else mod.values = [options.modification[key].toString()]
1125
1401
  }
@@ -1168,14 +1444,20 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1168
1444
  }
1169
1445
  client.unbind()
1170
1446
  } catch (err) {
1171
- scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest method=${method} base=${base} ldapOptions=${JSON.stringify(options)} Error Response = ${err.message}`)
1447
+ if (options.filter && typeof options.filter === 'object') {
1448
+ options.filter = options.filter.toString()
1449
+ }
1450
+ scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest method=${method} base=${berDecodeDn(base)} ldapOptions=${JSON.stringify(options)} Error Response = ${err.message}`)
1172
1451
  if (client) {
1173
- try { client.destroy() } catch (err) {}
1452
+ try { client.destroy() } catch (err) { }
1174
1453
  }
1175
1454
  throw err
1176
1455
  }
1177
1456
 
1178
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest method=${method} base=${base} ldapOptions=${JSON.stringify(options)} Response=${JSON.stringify(result)}`)
1457
+ if (options.filter && typeof options.filter === 'object') {
1458
+ options.filter = options.filter.toString()
1459
+ }
1460
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest method=${method} base=${berDecodeDn(base)} ldapOptions=${JSON.stringify(options)} Response=${JSON.stringify(result)}`)
1179
1461
  return result
1180
1462
  } // doRequest
1181
1463
 
@@ -1186,3 +1468,110 @@ process.on('SIGTERM', () => { // kill
1186
1468
  })
1187
1469
  process.on('SIGINT', () => { // Ctrl+C
1188
1470
  })
1471
+
1472
+ //
1473
+ // startup initialization
1474
+ // at the end to ensure scimgatway logger have started
1475
+ //
1476
+ if (!config?.map?.user) {
1477
+ scimgateway.logger.error('configuration map.user is missing')
1478
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1479
+ } else {
1480
+ let idFound = false
1481
+ let userNameFound = false
1482
+ for (const key in config.map.user) {
1483
+ if (config.map.user[key].mapTo === 'id') idFound = true
1484
+ else if (['userName', 'externalId'].includes(config.map.user[key].mapTo)) userNameFound = true
1485
+ if (idFound && userNameFound) break
1486
+ }
1487
+ if (!idFound || !userNameFound) {
1488
+ scimgateway.logger.error('configuration map.user missing mapTo definition for mandatory id/userName')
1489
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1490
+ }
1491
+ }
1492
+ if (!config?.map?.group) {
1493
+ scimgateway.logger.info('configuration map.group is not defiend and groups will not be supported')
1494
+ } else {
1495
+ let idFound = false
1496
+ let displayNameFound = false
1497
+ for (const key in config.map.group) {
1498
+ if (config.map.group[key].mapTo === 'id') idFound = true
1499
+ else if (config.map.group[key].mapTo === 'displayName') displayNameFound = true
1500
+ if (idFound && displayNameFound) break
1501
+ }
1502
+ if ((!idFound || !displayNameFound) && (Object.keys(config.map.group).length > 0)) {
1503
+ scimgateway.logger.error('configuration map.group missing mapTo definition for mandatory id/displayName')
1504
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1505
+ }
1506
+ }
1507
+
1508
+ for (const key in config.entity) {
1509
+ const userBase = config.entity[key]?.ldap?.userBase
1510
+ const groupBase = config.entity[key]?.ldap?.groupBase
1511
+ if (!userBase) {
1512
+ scimgateway.logger.error(`configuration missing mandatory endpoint.entity.${key}.ldap.userBase`)
1513
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1514
+ }
1515
+ if (!groupBase && config?.map?.group && Object.keys(config.map.group).length > 0) {
1516
+ scimgateway.logger.error(`configuration missing mandatory endpoint.entity.${key}.ldap.groupBase`)
1517
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1518
+ }
1519
+ let usrArr = config.entity[key]?.ldap?.namingAttribute?.user
1520
+ if (!usrArr || !Array.isArray(usrArr)) { // check for legacy
1521
+ const attr = config.entity[key]?.ldap?.userNamingAttr
1522
+ if (attr) {
1523
+ usrArr = [{ attribute: attr, mapTo: 'userName' }]
1524
+ if (!config.entity[key].ldap.namingAttribute) config.entity[key].ldap.namingAttribute = {}
1525
+ config.entity[key].ldap.namingAttribute.user = scimgateway.copyObj(usrArr)
1526
+ }
1527
+ }
1528
+ if (!Array.isArray(usrArr) || usrArr.length !== 1) {
1529
+ scimgateway.logger.error(`configuration missing namingAttribute: endpoint.entity.${key}.ldap.namingAttribute.user`)
1530
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1531
+ }
1532
+ if (!usrArr[0].attribute || !usrArr[0].mapTo) {
1533
+ scimgateway.logger.error(`configuration missing attribute/mapTo: endpoint.entity.${key}.ldap.namingAttribute.user`)
1534
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1535
+ }
1536
+ let grpArr = config.entity[key]?.ldap?.namingAttribute?.group
1537
+ if (config?.map?.group && Object.keys(config.map.group).length > 0) {
1538
+ if (!grpArr || !Array.isArray(grpArr)) { // check for legacy
1539
+ const attr = config.entity[key]?.ldap?.groupNamingAttr
1540
+ if (attr) {
1541
+ grpArr = [{ attribute: attr, mapTo: 'displayName' }]
1542
+ if (!config.entity[key].ldap.namingAttribute) config.entity[key].ldap.namingAttribute = {}
1543
+ config.entity[key].ldap.namingAttribute.group = scimgateway.copyObj(grpArr)
1544
+ }
1545
+ }
1546
+ if (!Array.isArray(grpArr) || grpArr.length !== 1) {
1547
+ scimgateway.logger.error(`configuration missing namingAttribute: endpoint.entity.${key}.ldap.namingAttribute.group`)
1548
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1549
+ }
1550
+ if (!grpArr[0].attribute || !grpArr[0].mapTo) {
1551
+ scimgateway.logger.error(`configuration missing attribute/mapTo: endpoint.entity.${key}.ldap.namingAttribute.group`)
1552
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1553
+ }
1554
+ }
1555
+ }
1556
+
1557
+ config.useSID_id = config.map.user.objectSid && config.map.user.objectSid.mapTo === 'id' // AD proprietary SID/GUID
1558
+ config.useGUID_id = config.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id'
1559
+ if (config.useSID_id && config.map.group) {
1560
+ if (!config.map.group.objectSid || config.map.group.objectSid.mapTo !== 'id') {
1561
+ scimgateway.logger.error('configuration missing group.objectSid - user and group should be using the same attribute')
1562
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1563
+ }
1564
+ } else if (config.useGUID_id && config.map.group) {
1565
+ if (!config.map.group.objectGUID || config.map.group.objectGUID.mapTo !== 'id') {
1566
+ scimgateway.logger.error('configuration missing group.objectGUID - user and group should be using the same attribute')
1567
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1568
+ }
1569
+ }
1570
+ if (config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) { // support mapping different inbound/outbound upn domain names
1571
+ if (config.map.user.userPrincipalName.mapDomain.inbound && config.map.user.userPrincipalName.mapDomain.outbound) {
1572
+ const inbound = config.map.user.userPrincipalName.mapDomain.inbound
1573
+ const outbound = config.map.user.userPrincipalName.mapDomain.outbound
1574
+ config.map.user.userPrincipalName.mapDomain.inbound = inbound.startsWith('@') ? inbound : '@' + inbound // "@my-company.com"
1575
+ config.map.user.userPrincipalName.mapDomain.outbound = outbound.startsWith('@') ? outbound : '@' + outbound // "@test.onmicrosoft.com
1576
+ } else delete config.map.user.userPrincipalName.mapDomain
1577
+ }
@@ -521,8 +521,8 @@ const ScimGateway = function () {
521
521
  else expires = oAuthTokenExpire
522
522
  config.auth.oauthTokenStore[authToken] = {
523
523
  expireDate: Date.now() + expires * 1000,
524
- readOnly: readOnly,
525
- baseEntities: baseEntities
524
+ readOnly,
525
+ baseEntities
526
526
  }
527
527
  return resolve(true)
528
528
  }
@@ -792,8 +792,8 @@ const ScimGateway = function () {
792
792
 
793
793
  config.auth.oauthTokenStore[token] = { // update token store
794
794
  expireDate: dtNow + expires * 1000, // 1 hour
795
- readOnly: readOnly,
796
- baseEntities: baseEntities
795
+ readOnly,
796
+ baseEntities
797
797
  }
798
798
 
799
799
  const tx = {
@@ -844,7 +844,10 @@ const ScimGateway = function () {
844
844
  let u = ctx.request.originalUrl.substr(0, ctx.request.originalUrl.lastIndexOf('/'))
845
845
  u = u.substr(u.lastIndexOf('/') + 1) // u = Users, Groups
846
846
  const handle = handler[u]
847
- const id = decodeURIComponent(require('path').basename(ctx.params.id, '.json')) // supports <id>.json
847
+ let id = decodeURIComponent(ctx.params.id)
848
+ if (id && id.endsWith('.json')) {
849
+ id = decodeURIComponent(require('path').basename(id, '.json')) // supports <id>.json
850
+ }
848
851
 
849
852
  const getObj = {
850
853
  attribute: 'id',
@@ -863,7 +866,7 @@ const ScimGateway = function () {
863
866
  handle: handle.getMethod,
864
867
  baseEntity: ctx.params.baseEntity,
865
868
  obj: ob,
866
- attributes: attributes,
869
+ attributes,
867
870
  ctxPassThrough: ctx.passThrough
868
871
  }
869
872
  logger.debug(`${gwName}[${pluginName}] publishing "${handle.getMethod}" to SCIM Stream and awaiting result`)
@@ -909,7 +912,7 @@ const ScimGateway = function () {
909
912
  handle: handler.groups.getMethod,
910
913
  baseEntity: ctx.params.baseEntity,
911
914
  obj: ob,
912
- attributes: attributes,
915
+ attributes,
913
916
  ctxPassThrough: ctx.passThrough
914
917
  }
915
918
  logger.debug(`${gwName}[${pluginName}] publishing "${handler.groups.getMethod}" to SCIM Stream and awaiting result - groups to be included`)
@@ -987,7 +990,6 @@ const ScimGateway = function () {
987
990
  getObj.value = decodeURIComponent(arrFilter.slice(2).join(' ').replace(/"/g, '')) // bjensen
988
991
  }
989
992
  }
990
-
991
993
  let err
992
994
  if (getObj.attribute) {
993
995
  if (multiValueTypes.includes(getObj.attribute) || getObj.attribute === 'roles') {
@@ -1108,7 +1110,7 @@ const ScimGateway = function () {
1108
1110
  handle: handle.getMethod,
1109
1111
  baseEntity: ctx.params.baseEntity,
1110
1112
  obj: ob,
1111
- attributes: attributes,
1113
+ attributes,
1112
1114
  ctxPassThrough: ctx.passThrough
1113
1115
  }
1114
1116
  logger.debug(`${gwName}[${pluginName}] publishing "${handle.getMethod}" to SCIM Stream and awaiting result`)
@@ -1147,7 +1149,7 @@ const ScimGateway = function () {
1147
1149
  handle: handler.groups.getMethod,
1148
1150
  baseEntity: ctx.params.baseEntity,
1149
1151
  obj: ob,
1150
- attributes: attributes,
1152
+ attributes,
1151
1153
  ctxPassThrough: ctx.passThrough
1152
1154
  }
1153
1155
  logger.debug(`${gwName}[${pluginName}] publishing "${handler.groups.getMethod}" to SCIM Stream and awaiting result - groups to be included`)
@@ -1265,13 +1267,13 @@ const ScimGateway = function () {
1265
1267
  res = await this.publish(streamObj)
1266
1268
  } else {
1267
1269
  if (scimdata.groups && Array.isArray(scimdata.groups) && handle.createMethod === 'createUser') {
1268
- if (!config.scim.groupMemberOfUser) {
1269
- for (let i = 0; i < scimdata.groups.length; i++) {
1270
- if (!scimdata.groups[i].value) continue
1271
- addGrps.push(decodeURIComponent(scimdata.groups[i].value))
1272
- }
1273
- delete scimdata.groups
1270
+ if (!config.scim.groupMemberOfUser) {
1271
+ for (let i = 0; i < scimdata.groups.length; i++) {
1272
+ if (!scimdata.groups[i].value) continue
1273
+ addGrps.push(decodeURIComponent(scimdata.groups[i].value))
1274
1274
  }
1275
+ delete scimdata.groups
1276
+ }
1275
1277
  }
1276
1278
  logger.debug(`${gwName}[${pluginName}] calling "${handle.createMethod}" and awaiting result`)
1277
1279
  res = await this[handle.createMethod](ctx.params.baseEntity, scimdata, ctx.passThrough)
@@ -1279,7 +1281,7 @@ const ScimGateway = function () {
1279
1281
  for (const key in res) { // merge any result e.g: {'id': 'xxxx'}
1280
1282
  jsonBody[key] = res[key]
1281
1283
  }
1282
-
1284
+
1283
1285
  if (!jsonBody.id) { // retrieve all attributes including id
1284
1286
  let res
1285
1287
  try {
@@ -1293,7 +1295,7 @@ const ScimGateway = function () {
1293
1295
  handle: handle.getMethod,
1294
1296
  baseEntity: ctx.params.baseEntity,
1295
1297
  obj: ob,
1296
- attributes: attributes,
1298
+ attributes,
1297
1299
  ctxPassThrough: ctx.passThrough
1298
1300
  }
1299
1301
  res = await this.publish(streamObj)
@@ -1310,7 +1312,7 @@ const ScimGateway = function () {
1310
1312
  handle: handle.getMethod,
1311
1313
  baseEntity: ctx.params.baseEntity,
1312
1314
  obj: ob,
1313
- attributes: attributes,
1315
+ attributes,
1314
1316
  ctxPassThrough: ctx.passThrough
1315
1317
  }
1316
1318
  res = await this.publish(streamObj)
@@ -1327,7 +1329,7 @@ const ScimGateway = function () {
1327
1329
  }
1328
1330
 
1329
1331
  if (addGrps.length > 0 && handle.createMethod === 'createUser') { // add group membership
1330
- const addGroups = async (groupId) => {
1332
+ const addGroups = async (groupId) => {
1331
1333
  if (config.stream.publisher.enabled) {
1332
1334
  const streamObj = {
1333
1335
  handle: handler.groups.modifyMethod,
@@ -1349,7 +1351,7 @@ const ScimGateway = function () {
1349
1351
  }
1350
1352
  jsonBody.groups = []
1351
1353
  addGrps.forEach((el) => {
1352
- jsonBody.groups.push({ 'value': el, 'type': 'direct' })
1354
+ jsonBody.groups.push({ value: el, type: 'direct' })
1353
1355
  })
1354
1356
  }
1355
1357
 
@@ -1398,7 +1400,7 @@ const ScimGateway = function () {
1398
1400
  const streamObj = {
1399
1401
  handle: handle.deleteMethod,
1400
1402
  baseEntity: ctx.params.baseEntity,
1401
- id: id,
1403
+ id,
1402
1404
  ctxPassThrough: ctx.passThrough
1403
1405
  }
1404
1406
  logger.debug(`${gwName}[${pluginName}] publishing "${handle.deleteMethod}" to SCIM Stream and awaiting result`)
@@ -1479,7 +1481,7 @@ const ScimGateway = function () {
1479
1481
  obj.value = decodeURIComponent(obj.value)
1480
1482
  groups.push(obj)
1481
1483
  }
1482
- delete scimdata.groups
1484
+ delete scimdata.groups
1483
1485
  }
1484
1486
  }
1485
1487
  try {
@@ -1487,7 +1489,7 @@ const ScimGateway = function () {
1487
1489
  let streamObj = {
1488
1490
  handle: handle.modifyMethod,
1489
1491
  baseEntity: ctx.params.baseEntity,
1490
- id: id,
1492
+ id,
1491
1493
  obj: scimdata,
1492
1494
  ctxPassThrough: ctx.passThrough
1493
1495
  }
@@ -1497,7 +1499,7 @@ const ScimGateway = function () {
1497
1499
  handle: 'replaceUsrGrp',
1498
1500
  baseEntity: ctx.params.baseEntity,
1499
1501
  originalUrl: ctx.request.originalUrl,
1500
- id: id,
1502
+ id,
1501
1503
  obj: scimdata,
1502
1504
  ctxPassThrough: ctx.passThrough
1503
1505
  }
@@ -1515,21 +1517,21 @@ const ScimGateway = function () {
1515
1517
  }
1516
1518
 
1517
1519
  if (groups.length > 0 && handle.modifyMethod === 'modifyUser') { // modify user includes groups, add/remove group membership
1518
- const updateGroup = async (groupsObj) => {
1520
+ const updateGroup = async (groupsObj) => {
1519
1521
  const groupId = groupsObj.value
1520
- const memberObj = { 'value': id }
1521
- if (groupsObj.operation) memberObj.operation= groupsObj.operation
1522
+ const memberObj = { value: id }
1523
+ if (groupsObj.operation) memberObj.operation = groupsObj.operation
1522
1524
  if (config.stream.publisher.enabled) {
1523
1525
  const streamObj = {
1524
1526
  handle: handler.groups.modifyMethod,
1525
1527
  baseEntity: ctx.params.baseEntity,
1526
1528
  id: groupId,
1527
- obj: { members: [ memberObj ] },
1529
+ obj: { members: [memberObj] },
1528
1530
  ctxPassThrough: ctx.passThrough
1529
1531
  }
1530
1532
  return await this.publish(streamObj)
1531
1533
  } else {
1532
- return await this[handler.groups.modifyMethod](ctx.params.baseEntity, groupId, { members: [ memberObj ] }, ctx.passThrough)
1534
+ return await this[handler.groups.modifyMethod](ctx.params.baseEntity, groupId, { members: [memberObj] }, ctx.passThrough)
1533
1535
  }
1534
1536
  }
1535
1537
  const res = await Promise.allSettled(groups.map((groupsObj) => updateGroup(groupsObj)))
@@ -1554,7 +1556,7 @@ const ScimGateway = function () {
1554
1556
  handle: handle.getMethod,
1555
1557
  baseEntity: ctx.params.baseEntity,
1556
1558
  obj: ob,
1557
- attributes: attributes,
1559
+ attributes,
1558
1560
  ctxPassThrough: ctx.passThrough
1559
1561
  }
1560
1562
  logger.debug(`${gwName}[${pluginName}] publishing "${handle.getMethod}" to SCIM Stream and awaiting result`)
@@ -1762,7 +1764,7 @@ const ScimGateway = function () {
1762
1764
  const streamObj = {
1763
1765
  handle: 'replaceUsrGrp',
1764
1766
  baseEntity: ctx.params.baseEntity,
1765
- originalUrl: originalUrl,
1767
+ originalUrl,
1766
1768
  id: ctx.params.id,
1767
1769
  obj: ctx.request.body,
1768
1770
  ctxPassThrough: ctx.passThrough
@@ -1810,12 +1812,12 @@ const ScimGateway = function () {
1810
1812
  result = await this.postApi(ctx.params.baseEntity, apiObj, ctx.passThrough)
1811
1813
  }
1812
1814
  if (result) {
1813
- if (typeof result === 'object') result = { result: result }
1815
+ if (typeof result === 'object') result = { result }
1814
1816
  else {
1815
1817
  try {
1816
1818
  result = { result: JSON.parse(result) }
1817
1819
  } catch (err) {
1818
- result = { result: result }
1820
+ result = { result }
1819
1821
  }
1820
1822
  }
1821
1823
  } else result = {}
@@ -1859,7 +1861,7 @@ const ScimGateway = function () {
1859
1861
  const streamObj = {
1860
1862
  handle: 'putApi',
1861
1863
  baseEntity: ctx.params.baseEntity,
1862
- id: id,
1864
+ id,
1863
1865
  obj: apiObj,
1864
1866
  ctxPassThrough: ctx.passThrough
1865
1867
  }
@@ -1870,12 +1872,12 @@ const ScimGateway = function () {
1870
1872
  result = await this.putApi(ctx.params.baseEntity, id, apiObj, ctx.passThrough)
1871
1873
  }
1872
1874
  if (result) {
1873
- if (typeof result === 'object') result = { result: result }
1875
+ if (typeof result === 'object') result = { result }
1874
1876
  else {
1875
1877
  try {
1876
1878
  result = { result: JSON.parse(result) }
1877
1879
  } catch (err) {
1878
- result = { result: result }
1880
+ result = { result }
1879
1881
  }
1880
1882
  }
1881
1883
  } else result = {}
@@ -1919,7 +1921,7 @@ const ScimGateway = function () {
1919
1921
  const streamObj = {
1920
1922
  handle: 'patchApi',
1921
1923
  baseEntity: ctx.params.baseEntity,
1922
- id: id,
1924
+ id,
1923
1925
  obj: apiObj,
1924
1926
  ctxPassThrough: ctx.passThrough
1925
1927
  }
@@ -1930,12 +1932,12 @@ const ScimGateway = function () {
1930
1932
  result = await this.patchApi(ctx.params.baseEntity, id, apiObj, ctx.passThrough)
1931
1933
  }
1932
1934
  if (result) {
1933
- if (typeof result === 'object') result = { result: result }
1935
+ if (typeof result === 'object') result = { result }
1934
1936
  else {
1935
1937
  try {
1936
1938
  result = { result: JSON.parse(result) }
1937
1939
  } catch (err) {
1938
- result = { result: result }
1940
+ result = { result }
1939
1941
  }
1940
1942
  }
1941
1943
  } else result = {}
@@ -1976,7 +1978,7 @@ const ScimGateway = function () {
1976
1978
  const streamObj = {
1977
1979
  handle: 'getApi',
1978
1980
  baseEntity: ctx.params.baseEntity,
1979
- id: id,
1981
+ id,
1980
1982
  query: ctx.query,
1981
1983
  obj: apiObj,
1982
1984
  ctxPassThrough: ctx.passThrough
@@ -1988,12 +1990,12 @@ const ScimGateway = function () {
1988
1990
  result = await this.getApi(ctx.params.baseEntity, id, ctx.query, apiObj, ctx.passThrough)
1989
1991
  }
1990
1992
  if (result) {
1991
- if (typeof result === 'object') result = { result: result }
1993
+ if (typeof result === 'object') result = { result }
1992
1994
  else {
1993
1995
  try {
1994
1996
  result = { result: JSON.parse(result) }
1995
1997
  } catch (err) {
1996
- result = { result: result }
1998
+ result = { result }
1997
1999
  }
1998
2000
  }
1999
2001
  } else result = {}
@@ -2026,7 +2028,7 @@ const ScimGateway = function () {
2026
2028
  const streamObj = {
2027
2029
  handle: 'deleteApi',
2028
2030
  baseEntity: ctx.params.baseEntity,
2029
- id: id,
2031
+ id,
2030
2032
  ctxPassThrough: ctx.passThrough
2031
2033
  }
2032
2034
  logger.debug(`${gwName}[${pluginName}] publishing "deleteApi" to SCIM Stream and awaiting result`)
@@ -2036,12 +2038,12 @@ const ScimGateway = function () {
2036
2038
  result = await this.deleteApi(ctx.params.baseEntity, id, ctx.passThrough)
2037
2039
  }
2038
2040
  if (result) {
2039
- if (typeof result === 'object') result = { result: result }
2041
+ if (typeof result === 'object') result = { result }
2040
2042
  else {
2041
2043
  try {
2042
2044
  result = { result: JSON.parse(result) }
2043
2045
  } catch (err) {
2044
- result = { result: result }
2046
+ result = { result }
2045
2047
  }
2046
2048
  }
2047
2049
  } else result = {}
@@ -3323,7 +3325,7 @@ const jsonErr = (scimVersion, pluginName, htmlErrCode, err) => {
3323
3325
  errJson =
3324
3326
  {
3325
3327
  schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
3326
- scimType: scimType,
3328
+ scimType,
3327
3329
  detail: msg,
3328
3330
  status: customErrCode || htmlErrCode
3329
3331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.5.7",
3
+ "version": "4.5.8",
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",
@@ -43,7 +43,7 @@
43
43
  "ldapjs": "^3.0.7",
44
44
  "lokijs": "^1.5.12",
45
45
  "mongodb": "^6.6.2",
46
- "nats": "^2.26.0",
46
+ "nats": "^2.28.2",
47
47
  "node-machine-id": "1.1.9",
48
48
  "nodemailer": "^6.9.13",
49
49
  "passport": "^0.7.0",