scimgateway 4.5.7 → 4.5.9

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
@@ -65,7 +65,7 @@ Can be used to chain several gateways
65
65
 
66
66
  * **Soap** (SOAP Webservice)
67
67
  Demonstrates user provisioning towards SOAP-Based endpoint
68
- Excample WSDLs are included
68
+ Example WSDLs are included
69
69
  Using endpoint "Forwardinc" as an example (comes with Symantec/Broadcom/CA IM SDK - SDKWS)
70
70
  Shows how to implement a highly configurable multi tenant or multi endpoint solution through `baseEntity` in URL
71
71
 
@@ -84,7 +84,7 @@ Includes Symantec/Broadcom/CA ConnectorXpress metafile for creating provisioning
84
84
  * **LDAP** (Directory)
85
85
  Fully functional LDAP plugin
86
86
  Pre-configured for Microsoft Active Directory
87
- Using endpointMapper (like plugin-entra-id) for attribute flexibility
87
+ Using endpointMapper (like plugin-entra-id) for attribute mapping flexibility
88
88
 
89
89
  * **API** (REST Webservices)
90
90
  Demonstrates API Gateway/plugin functionality using post/put/patch/get/delete
@@ -1163,6 +1163,54 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1163
1163
 
1164
1164
  ## Change log
1165
1165
 
1166
+ ### v4.5.9
1167
+
1168
+ [Improved]
1169
+
1170
+ - Dependencies bump
1171
+
1172
+ ### v4.5.8
1173
+
1174
+ [Fixed]
1175
+
1176
+ - plugin-ldap failed when using national special characters and some other LDAP special characters in DN
1177
+
1178
+ Note, plugin-ldap now has following new configuration:
1179
+
1180
+ "ldap": {
1181
+ "isOpenLdap": false,
1182
+ ...
1183
+ "namingAttribute": {
1184
+ "user": [
1185
+ {
1186
+ "attribute": "CN",
1187
+ "mapTo": "userName"
1188
+ }
1189
+ ],
1190
+ "group": [
1191
+ {
1192
+ "attribute": "CN",
1193
+ "mapTo": "displayName"
1194
+ }
1195
+ ]
1196
+ },
1197
+ ...
1198
+ }
1199
+
1200
+ `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.
1201
+
1202
+ `namingAttribute` can now be linked to scim `mapTo` attribute and is not hardcoded like it was in previous version.
1203
+
1204
+ Previous `userNamingAttr` and `groupNamingAttr` shown below, is now deprecated
1205
+
1206
+ "ldap": {
1207
+ ...
1208
+ "userNamingAttr": "CN",
1209
+ "groupNamingAttr": "CN",
1210
+ ...
1211
+ }
1212
+
1213
+
1166
1214
  ### v4.5.7
1167
1215
 
1168
1216
  [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",
package/lib/plugin-api.js CHANGED
@@ -283,8 +283,8 @@ const getAccessToken = async (baseEntity, ctx) => {
283
283
  lock.release()
284
284
  throw (err)
285
285
  }
286
- if (config.entity[baseEntity].tokenAuth) { // in case response using token instead of access_token
287
- if (jbody.token) jbody.access_token = jbody.token
286
+ if (config.entity[baseEntity].tokenAuth) { // custom access_token
287
+ if (jbody.accessToken) jbody.access_token = jbody.accessToken
288
288
  }
289
289
  if (!jbody.access_token) {
290
290
  const err = new Error(`[${action}] Error message: Retrieved invalid token response`)
@@ -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
 
@@ -229,8 +209,6 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
229
209
  const users = await doRequest(baseEntity, method, base, ldapOptions, ctx) // ignoring SCIM paging startIndex/count - get all
230
210
  result.totalResults = users.length
231
211
  result.Resources = await Promise.all(users.map(async (user) => { // Promise.all because of async map
232
- if (user.name) delete user.name // because mapper converts to SCIM name.xxx
233
-
234
212
  // endpoint spesific attribute handling
235
213
  // "active" must be handled separate
236
214
  if (user.userAccountControl !== undefined) { // SCIM "active" - Active Directory
@@ -263,7 +241,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
263
241
  }
264
242
 
265
243
  const scimObj = scimgateway.endpointMapper('inbound', user, config.map.user)[0] // endpoint attribute naming => SCIM
266
- if (!scimObj.groups) scimObj.groups = []
244
+ // if (!scimObj.groups) scimObj.groups = []
267
245
  return scimObj
268
246
  }))
269
247
  } catch (err) {
@@ -317,8 +295,16 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
317
295
  // endpointObj.objectClass is mandatory and must must match your ldap schema
318
296
  endpointObj.objectClass = config.entity[baseEntity].ldap.userObjectClasses // Active Directory: ["user", "person", "organizationalPerson", "top"]
319
297
 
298
+ let base = ''
299
+ const [userNamingAttr, scimAttr] = getNamingAttribute(baseEntity, 'user') // ['CN', 'userName']
300
+ const arr = scimAttr.split('.')
301
+ if (arr.length < 2) {
302
+ base = `${userNamingAttr}=${userObj[scimAttr]},${userBase}`
303
+ } else {
304
+ base = `${userNamingAttr}=${userObj[arr[0]][arr[1]]},${userBase}`
305
+ }
306
+
320
307
  const method = 'add'
321
- const base = `${config.entity[baseEntity].ldap.userNamingAttr}=${userObj.userName},${userBase}`
322
308
  const ldapOptions = endpointObj
323
309
 
324
310
  try {
@@ -371,7 +357,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
371
357
  delete attrObj.groups // make sure to be removed from attrObj
372
358
 
373
359
  const [groupsAttr] = scimgateway.endpointMapper('outbound', 'groups.value', config.map.user)
374
- const grp = { add: { }, remove: { } }
360
+ const grp = { add: {}, remove: {} }
375
361
  grp.add[groupsAttr] = []
376
362
  grp.remove[groupsAttr] = []
377
363
 
@@ -510,8 +496,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
510
496
  totalResults: null
511
497
  }
512
498
 
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`)
499
+ if (!config?.map?.group || !config.entity[baseEntity]?.ldap?.groupBase) { // not using groups
500
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] "${action}" skip group handling - missing configuration endpoint.map.group or groupBase`)
515
501
  return result
516
502
  }
517
503
 
@@ -536,7 +522,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
536
522
  // mandatory if-else logic - start
537
523
  if (getObj.operator) {
538
524
  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
525
+ // mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
540
526
  if (getObj.attribute === 'id') { // lookup using dn or objectSid/objectGUID (Active Directory)
541
527
  if (config.useSID_id) {
542
528
  const sid = convertStringToSid(getObj.value) // sid using formatted string instead of default hex
@@ -566,12 +552,12 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
566
552
  attributes: attrs
567
553
  }
568
554
  } else { // search instead of lookup
555
+ const filter = createAndFilter(baseEntity, 'group', [{ attribute: groupIdAttr, value: getObj.value }])
569
556
  ldapOptions = {
570
- filter: `&${getObjClassFilter(baseEntity, 'group')}(${groupIdAttr}=${getObj.value})`, // &(objectClass=group)(cn=Group1)
571
- scope: scope,
557
+ filter,
558
+ scope,
572
559
  attributes: attrs
573
560
  }
574
- if (config.entity[baseEntity].ldap.groupFilter) ldapOptions.filter += config.entity[baseEntity].ldap.groupFilter
575
561
  }
576
562
  }
577
563
  } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
@@ -580,19 +566,30 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
580
566
  ldapOptions = 'getMemberOfGroups'
581
567
  } else {
582
568
  // optional - simpel filtering
583
- throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
569
+ if (getObj.operator === 'eq') {
570
+ const [filterAttr, err] = scimgateway.endpointMapper('outbound', getObj.attribute, config.map.group)
571
+ if (err) throw new Error(`${action} error: ${err.message}`)
572
+ const filter = createAndFilter(baseEntity, 'group', [{ attribute: filterAttr, value: getObj.value }])
573
+ ldapOptions = {
574
+ filter,
575
+ scope,
576
+ attributes: attrs
577
+ }
578
+ } else {
579
+ throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
580
+ }
584
581
  }
585
582
  } else if (getObj.rawFilter) {
586
583
  // optional - advanced filtering having and/or/not - use getObj.rawFilter
587
584
  throw new Error(`${action} error: not supporting advanced filtering: ${getObj.rawFilter}`)
588
585
  } else {
589
- // mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all groups to be returned - correspond to exploreGroups() in versions < 4.x.x
586
+ // mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all groups to be returned - correspond to exploreGroups() in versions < 4.x.x
587
+ const filter = createAndFilter(baseEntity, 'group', [{ attribute: groupDisplayNameAttr, value: '*' }])
590
588
  ldapOptions = {
591
- filter: `&${getObjClassFilter(baseEntity, 'group')}(${groupDisplayNameAttr}=*)`,
592
- scope: scope,
589
+ filter,
590
+ scope,
593
591
  attributes: attrs
594
592
  }
595
- if (config.entity[baseEntity].ldap.groupFilter) ldapOptions.filter += config.entity[baseEntity].ldap.groupFilter
596
593
  }
597
594
  // mandatory if-else logic - end
598
595
 
@@ -639,6 +636,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
639
636
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
640
637
 
641
638
  if (!config.map.group) throw new Error(`${action} error: missing configuration endpoint.map.group`)
639
+ const groupBase = config.entity[baseEntity].ldap.groupBase
642
640
 
643
641
  // convert SCIM attributes to endpoint attributes according to config.map
644
642
  const [endpointObj] = scimgateway.endpointMapper('outbound', groupObj, config.map.group)
@@ -646,13 +644,23 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
646
644
  // endpointObj.objectClass is mandatory and must must match your ldap schema
647
645
  endpointObj.objectClass = config.entity[baseEntity].ldap.groupObjectClasses // Active Directory: ["group"]
648
646
 
647
+ let base = ''
648
+ const [groupNamingAttr, scimAttr] = getNamingAttribute(baseEntity, 'group') // ['CN', 'displayName']
649
+ const arr = scimAttr.split('.')
650
+ if (arr.length < 2) {
651
+ base = `${groupNamingAttr}=${groupObj[scimAttr]},${groupBase}`
652
+ } else {
653
+ base = `${groupNamingAttr}=${groupObj[arr[0]][arr[1]]},${groupBase}`
654
+ }
655
+
649
656
  const method = 'add'
650
- const base = `${config.entity[baseEntity].ldap.groupNamingAttr}=${groupObj.displayName},${config.entity[baseEntity].ldap.groupBase}`
651
657
  const ldapOptions = endpointObj
652
658
 
653
659
  try {
654
660
  await doRequest(baseEntity, method, base, ldapOptions, ctx)
655
- return null
661
+ const res = await scimgateway.getGroups(baseEntity, { attribute: 'id', operator: 'eq', value: base }, [], ctx)
662
+ if (res && Array.isArray(res.Resources) && res.Resources.length === 1) return res.Resources[0]
663
+ else return null
656
664
  } catch (err) {
657
665
  const newErr = new Error(`${action} error: ${err.message}`)
658
666
  if (newErr.message.includes('ENTRY_EXISTS')) newErr.name += '#409' // customErrCode
@@ -703,7 +711,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
703
711
  const [memberAttr] = scimgateway.endpointMapper('outbound', 'members.value', config.map.group)
704
712
  if (!memberAttr && attrObj.members) throw new Error(`${action} error: missing attribute mapping configuration for group members`)
705
713
 
706
- const grp = { add: { }, remove: { } }
714
+ const grp = { add: {}, remove: {} }
707
715
  grp.add[memberAttr] = []
708
716
  grp.remove[memberAttr] = []
709
717
 
@@ -764,22 +772,82 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
764
772
  const _serviceClient = {}
765
773
 
766
774
  //
767
- // getObjClassFilter returns object classes to be included in search
775
+ // createAndFilter creates AndFilter object to be used as filter instead of standard string filter
776
+ // Using AndFilter object for eliminating internal ldapjs escaping problems related to values with some
777
+ // combinations of parentheses e.g. ab(c)d
768
778
  //
769
- const getObjClassFilter = (baseEntity, type) => {
770
- let filter = ''
779
+ const createAndFilter = (baseEntity, type, arrObj) => {
780
+ const objFilters = []
781
+
782
+ // add arrObj
783
+ for (let i = 0; i < arrObj.length; i++) {
784
+ if (arrObj[i].value.indexOf('*') > -1) { // SubstringFilter or PresenceFilter
785
+ const arr = arrObj[i].value.split('*')
786
+ if (arr.length === 2 && !arr[0] && !arr[1]) { // cn=*
787
+ const f = new ldap.PresenceFilter({ attribute: arrObj[i].attribute })
788
+ objFilters.push(f)
789
+ } else { // cn=ab*cd*e
790
+ const fObj = {
791
+ attribute: arrObj[i].attribute
792
+ }
793
+ const arrAny = []
794
+ fObj.initial = arr[0]
795
+ if (!fObj.initial) delete fObj.initial
796
+ for (let i = 1; i < arr.length - 1; i++) {
797
+ arrAny.push(arr[i])
798
+ }
799
+ fObj.any = arrAny
800
+ if (arr[arr.length - 1]) {
801
+ fObj.final = arr[arr.length - 1]
802
+ }
803
+ const f = new ldap.SubstringFilter(fObj)
804
+ objFilters.push(f)
805
+ }
806
+ } else { // EqualityFilter cn=abc
807
+ const f = new ldap.EqualityFilter({ attribute: arrObj[i].attribute, value: arrObj[i].value })
808
+ objFilters.push(f)
809
+ }
810
+ }
811
+
812
+ // add from configuration objectClass and userFiter/groupFilter
771
813
  switch (type) {
772
814
  case 'user':
773
815
  for (let i = 0; i < config.entity[baseEntity].ldap.userObjectClasses.length; i++) {
774
- filter += `(objectClass=${config.entity[baseEntity].ldap.userObjectClasses[i]})`
816
+ const f = new ldap.EqualityFilter({ attribute: 'objectClass', value: config.entity[baseEntity].ldap.userObjectClasses[i] })
817
+ objFilters.push(f)
818
+ }
819
+ if (config.entity[baseEntity].ldap.userFilter) {
820
+ try {
821
+ const uf = ldap.parseFilter(config.entity[baseEntity].ldap.userFilter)
822
+ objFilters.push(uf)
823
+ } catch (err) {
824
+ throw new Error(`configuration ldap.userFilter: ${config.entity[baseEntity].ldap.userFilter} - parseFilter error: ${err.message}`)
825
+ }
775
826
  }
776
827
  break
777
828
  case 'group':
778
829
  for (let i = 0; i < config.entity[baseEntity].ldap.groupObjectClasses.length; i++) {
779
- filter += `(objectClass=${config.entity[baseEntity].ldap.groupObjectClasses[i]})`
830
+ const f = new ldap.EqualityFilter({ attribute: 'objectClass', value: config.entity[baseEntity].ldap.groupObjectClasses[i] })
831
+ objFilters.push(f)
832
+ if (config.entity[baseEntity].ldap.groupFilter) {
833
+ try {
834
+ const gf = ldap.parseFilter(config.entity[baseEntity].ldap.groupFilter)
835
+ objFilters.push(gf)
836
+ } catch (err) {
837
+ throw new Error(`configuration ldap.groupFilter: ${config.entity[baseEntity].ldap.groupFilter} - parseFilter error: ${err.message}`)
838
+ }
839
+ }
780
840
  }
781
841
  break
782
842
  }
843
+
844
+ // put all into AndFilter
845
+ const filter = new ldap.AndFilter({
846
+ filters: [
847
+ ...objFilters
848
+ ]
849
+ })
850
+
783
851
  return filter
784
852
  }
785
853
 
@@ -858,10 +926,10 @@ const convertSidToString = (buf) => {
858
926
  for (i = 0, end = subAuthorityCount - 1, asc = end >= 0; asc ? i <= end : i >= end; asc ? i++ : i--) {
859
927
  const subAuthOffset = i * 4
860
928
  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))
929
+ pad(buf[11 + subAuthOffset].toString(16)) +
930
+ pad(buf[10 + subAuthOffset].toString(16)) +
931
+ pad(buf[9 + subAuthOffset].toString(16)) +
932
+ pad(buf[8 + subAuthOffset].toString(16))
865
933
  sidString += `-${parseInt(tmp, 16)}`
866
934
  }
867
935
  } catch (err) {
@@ -946,9 +1014,10 @@ const getMemberOfGroups = async (baseEntity, id, ctx) => {
946
1014
  const scope = 'sub'
947
1015
  const base = config.entity[baseEntity].ldap.groupBase
948
1016
 
1017
+ const filter = createAndFilter(baseEntity, 'group', [{ attribute: memberAttr, value: idDn }])
949
1018
  const ldapOptions = {
950
- filter: `&${getObjClassFilter(baseEntity, 'group')}(${memberAttr}=${idDn})`,
951
- scope: scope,
1019
+ filter,
1020
+ scope,
952
1021
  attributes: attrs
953
1022
  }
954
1023
 
@@ -967,6 +1036,192 @@ const getMemberOfGroups = async (baseEntity, id, ctx) => {
967
1036
  }
968
1037
  }
969
1038
 
1039
+ //
1040
+ // ldapEscDn will escape DN according to the LDAP standard adjusted to ldapjs behavior
1041
+ // using OpenLDAP, DN must be escaped - national characters and special ldap characters
1042
+ // using Active Directory (none OpenLDAP), DN should not be escaped, but DN retrieved from AD is character escaped
1043
+ //
1044
+ const ldapEscDn = (isOpenLdap, str) => {
1045
+ if (!str) return str
1046
+
1047
+ if (!isOpenLdap && str.indexOf('\\') > 0) {
1048
+ const conv = str.replace(/\\([0-9A-Fa-f]{2})/g, (_, hex) => {
1049
+ const intAscii = parseInt(hex, 16)
1050
+ if (intAscii > 128) { // extended ascii - will be unescaped by decodeURIComponent
1051
+ return '%' + hex
1052
+ } else { // use character escape
1053
+ return '\\' + String.fromCharCode(intAscii)
1054
+ }
1055
+ })
1056
+ str = decodeURIComponent(conv)
1057
+ str = str.replace(/\\/g, '') // lower ascii may be character escaped e.g. 'cn=Kürt\, Lastname' - see below comma logic
1058
+ }
1059
+
1060
+ const arr = str.split(',')
1061
+ for (let i = 0; i < arr.length; i++) { // CN=Firstname, Lastname,OU=...
1062
+ if (!arr[i]) { // value having comma only
1063
+ if (arr[i - 1].charAt(arr[i - 1].length - 1) === '\\') {
1064
+ arr[i - 1] = arr[i - 1].substring(0, arr[i - 1].length - 1)
1065
+ }
1066
+ if (isOpenLdap) arr[i - 1] += '\\,'
1067
+ else arr[i - 1] += ','
1068
+ arr.splice(i, 1)
1069
+ i -= 1
1070
+ continue
1071
+ }
1072
+ const a = arr[i].split('=')
1073
+ if (a.length < 2 && i > 0) { // value having comma and content
1074
+ if (arr[i - 1].charAt(arr[i - 1].length - 1) === '\\') {
1075
+ arr[i - 1] = arr[i - 1].substring(0, arr[i - 1].length - 1)
1076
+ }
1077
+ if (isOpenLdap) arr[i - 1] += `\\,${ldapEsc(a[0])}`
1078
+ else arr[i - 1] += `,${a[0]}`
1079
+ arr.splice(i, 1)
1080
+ i -= 1
1081
+ continue
1082
+ } else {
1083
+ if (isOpenLdap) arr[i] = `${a[0]}=${ldapEsc(a[1])}`
1084
+ else arr[i] = `${a[0]}=${a[1]}`
1085
+ }
1086
+ if (i > 0) break // only escape logic on first, assume sub OU's are correct
1087
+ }
1088
+ if (isOpenLdap) {
1089
+ str = arr.join(',')
1090
+ return str
1091
+ }
1092
+ // Using dn object and BER encoding
1093
+ // e.g., Active Directory to avoid internal ldapjs OpenLDAP validating and string escaping logic
1094
+ const dn = new ldap.DN()
1095
+ for (let i = 0; i < arr.length; i++) {
1096
+ const a = arr[i].split('=') // cn=Kürt
1097
+ if (a.length === 2) {
1098
+ if (i === 0) {
1099
+ const ua = new Uint8Array(Buffer.from(a[1], 'utf-8'))
1100
+ const buf = Buffer.from(new Uint8Array([4, ua.length, ...ua]))
1101
+ const rdn = {}
1102
+ rdn[a[0]] = new BerReader(buf)
1103
+ dn.push(new ldap.RDN(rdn))
1104
+ // new BerReader(Buffer.from([0x04, 0x05, 0x4B, 0xc3, 0xbc, 0x72, 0x74])) // Kürt
1105
+ // the leading 04 is the tag for "octet string" and the following 05 is the length in bytes of the string.
1106
+ } else {
1107
+ const rdn = {}
1108
+ rdn[a[0]] = a[1]
1109
+ dn.push(new ldap.RDN(rdn))
1110
+ }
1111
+ } else {
1112
+ throw new Error('ldapEscDn() invalid DN: ' + str)
1113
+ }
1114
+ }
1115
+ return dn
1116
+ }
1117
+
1118
+ //
1119
+ // ldapEsc will character escape str according to OpenLDAP DN standard
1120
+ // Hex encoded escaping (extended and unicode ascii) is not included because
1121
+ // automatically handled by ldapjs when not using BER encoded DN
1122
+ //
1123
+ const ldapEsc = (str) => {
1124
+ if (!str) return str
1125
+ let newStr = ''
1126
+ for (let i = 0; i < str.length; i++) {
1127
+ let c = str[i]
1128
+ let isEsc = false
1129
+ if (i > 0 && str[i - 1] === '\\') isEsc = true
1130
+ switch (c) {
1131
+ case ',':
1132
+ if (isEsc) c = ','
1133
+ else c = '\\,'
1134
+ break
1135
+ case ';':
1136
+ if (isEsc) c = ';'
1137
+ else c = '\\;'
1138
+ break
1139
+ case '+':
1140
+ if (isEsc) c = '+'
1141
+ else c = '\\+'
1142
+ break
1143
+ case '<':
1144
+ if (isEsc) c = '<'
1145
+ else c = '\\<'
1146
+ break
1147
+ case '>':
1148
+ if (isEsc) c = '>'
1149
+ else c = '\\>'
1150
+ break
1151
+ case '=':
1152
+ if (isEsc) c = '='
1153
+ else c = '\\='
1154
+ break
1155
+ case '"':
1156
+ if (isEsc) c = '"'
1157
+ else c = '\\"'
1158
+ break
1159
+ case '(':
1160
+ if (isEsc) c = '('
1161
+ else c = '\\('
1162
+ break
1163
+ case ')':
1164
+ if (isEsc) c = ')'
1165
+ else c = '\\)'
1166
+ break
1167
+ }
1168
+ newStr += c
1169
+ }
1170
+ return newStr
1171
+ }
1172
+
1173
+ //
1174
+ // berDecodeDn decodes a BER string part of type DN
1175
+ // OU=#04057573657273,OU=abc,... => OU=users,OU=abc,...
1176
+ // only using BER on first part of dn
1177
+ // Having BER decoding for Active Directory, but not for OpenLDAP
1178
+ //
1179
+ const berDecodeDn = (dn) => {
1180
+ if (Object.prototype.toString.call(dn) !== '[object LdapDn]') return dn // OpenLDAP
1181
+ const str = dn.toString()
1182
+ if (str.indexOf('#') < 1) return str
1183
+ const arr = str.split('#')
1184
+ if (arr.length === 2) {
1185
+ const a = arr[1].split(',')
1186
+ if (a.length > 1) {
1187
+ const berStr = a[0].substring(4)
1188
+ let decoded = ''
1189
+ let c = ''
1190
+ if (berStr.length % 2 === 0) {
1191
+ for (let i = 0; i < berStr.length; i++) {
1192
+ c += berStr[i]
1193
+ if (c.length === 2) {
1194
+ const intAscii = parseInt(c, 16)
1195
+ decoded += String.fromCharCode(intAscii)
1196
+ c = ''
1197
+ }
1198
+ }
1199
+ }
1200
+ if (decoded.length > 0) {
1201
+ a.splice(0, 1) // remove element 0 from array
1202
+ return `${arr[0]}${decoded},${a.join(',')}` // OU=users,OU=abc
1203
+ }
1204
+ }
1205
+ }
1206
+ return str
1207
+ }
1208
+
1209
+ const getNamingAttribute = (baseEntity, type) => {
1210
+ let arr
1211
+ switch (type) {
1212
+ case 'user':
1213
+ arr = config.entity[baseEntity]?.ldap?.namingAttribute?.user
1214
+ break
1215
+ case 'group':
1216
+ arr = config.entity[baseEntity]?.ldap?.namingAttribute?.group
1217
+ break
1218
+ default:
1219
+ throw new Error(`getNamingAttribute error: invalid type ${type}`)
1220
+ }
1221
+ if (!Array.isArray(arr) || arr.length !== 1) throw new Error(`configuration missing namingAttribute definition for ${type}`)
1222
+ return [arr[0].attribute, arr[0].mapTo]
1223
+ }
1224
+
970
1225
  //
971
1226
  // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
972
1227
  //
@@ -1034,11 +1289,11 @@ const getServiceClient = async (baseEntity, ctx) => {
1034
1289
  // "attributes": ["sAMAccountName","displayName","mail"]
1035
1290
  // }
1036
1291
  //
1037
- const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1292
+ const doRequest = async (baseEntity, method, base, options, ctx) => {
1038
1293
  let result = null
1039
1294
  let client = null
1295
+ base = ldapEscDn(config.entity[baseEntity].ldap.isOpenLdap, base)
1040
1296
 
1041
- const options = scimgateway.copyObj(ldapOptions)
1042
1297
  // support having different upn-domain on IdP and target
1043
1298
  if (options.modification && options.modification.userPrincipalName && config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) {
1044
1299
  if (options.modification.userPrincipalName.endsWith(config.map.user.userPrincipalName.mapDomain.outbound)) {
@@ -1055,7 +1310,8 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1055
1310
  options.paged = { pageSize: 200, pagePause: false } // parse entire directory calling 'page' method for each page
1056
1311
  result = await new Promise((resolve, reject) => {
1057
1312
  const results = []
1058
- client.search(base, scimgateway.copyObj(options), (err, search) => {
1313
+
1314
+ client.search(base, options, (err, search) => {
1059
1315
  if (err) {
1060
1316
  return reject(err)
1061
1317
  }
@@ -1086,6 +1342,23 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1086
1342
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] outbound upnMapDomain ${old} => ${obj.userPrincipalName}`)
1087
1343
  }
1088
1344
  }
1345
+
1346
+ if (obj.dn && obj.dn.indexOf('\\') > 0) {
1347
+ // for OpenLDAP ensure dn is not hex escaped e.g.: cn=K\c3\bcrt => cn=Kürt
1348
+ // because dn may be be used as value in standard attributes like group memberOf
1349
+ obj.dn = obj.dn.replace(/\\\\/g, '__') // temp
1350
+ let conv = obj.dn.replace(/\\([0-9A-Fa-f]{2})/g, (_, hex) => {
1351
+ const intAscii = parseInt(hex, 16)
1352
+ if (intAscii > 128) { // extended ascii - will be unescaped by decodeURIComponent
1353
+ return '%' + hex
1354
+ } else { // use character escape
1355
+ return '\\' + String.fromCharCode(intAscii)
1356
+ }
1357
+ })
1358
+ conv = conv.replace(/__/g, '\\\\')
1359
+ obj.dn = decodeURIComponent(conv)
1360
+ }
1361
+
1089
1362
  results.push(obj)
1090
1363
  })
1091
1364
 
@@ -1115,11 +1388,10 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1115
1388
  if (mod.values.length > 1) { // delete before replace to keep inbound order
1116
1389
  changes.push({
1117
1390
  operation: 'delete',
1118
- modification: {type: key, values: []}
1391
+ modification: { type: key, values: [] }
1119
1392
  })
1120
1393
  }
1121
- }
1122
- else {
1394
+ } else {
1123
1395
  if (typeof options.modification[key] === 'string') mod.values = [options.modification[key]]
1124
1396
  else mod.values = [options.modification[key].toString()]
1125
1397
  }
@@ -1168,14 +1440,20 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1168
1440
  }
1169
1441
  client.unbind()
1170
1442
  } catch (err) {
1171
- scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest method=${method} base=${base} ldapOptions=${JSON.stringify(options)} Error Response = ${err.message}`)
1443
+ if (options.filter && typeof options.filter === 'object') {
1444
+ options.filter = options.filter.toString()
1445
+ }
1446
+ scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest method=${method} base=${berDecodeDn(base)} ldapOptions=${JSON.stringify(options)} Error Response = ${err.message}`)
1172
1447
  if (client) {
1173
- try { client.destroy() } catch (err) {}
1448
+ try { client.destroy() } catch (err) { }
1174
1449
  }
1175
1450
  throw err
1176
1451
  }
1177
1452
 
1178
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest method=${method} base=${base} ldapOptions=${JSON.stringify(options)} Response=${JSON.stringify(result)}`)
1453
+ if (options.filter && typeof options.filter === 'object') {
1454
+ options.filter = options.filter.toString()
1455
+ }
1456
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest method=${method} base=${berDecodeDn(base)} ldapOptions=${JSON.stringify(options)} Response=${JSON.stringify(result)}`)
1179
1457
  return result
1180
1458
  } // doRequest
1181
1459
 
@@ -1186,3 +1464,121 @@ process.on('SIGTERM', () => { // kill
1186
1464
  })
1187
1465
  process.on('SIGINT', () => { // Ctrl+C
1188
1466
  })
1467
+
1468
+ //
1469
+ // startup initialization
1470
+ // at the end to ensure scimgatway logger have started
1471
+ //
1472
+ if (!config?.map?.user) {
1473
+ scimgateway.logger.error('configuration map.user is missing')
1474
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1475
+ } else {
1476
+ let idFound = false
1477
+ let userNameFound = false
1478
+ for (const key in config.map.user) {
1479
+ if (config.map.user[key].mapTo === 'id') idFound = true
1480
+ else if (['userName', 'externalId'].includes(config.map.user[key].mapTo)) userNameFound = true
1481
+ if (idFound && userNameFound) break
1482
+ }
1483
+ if (!idFound || !userNameFound) {
1484
+ scimgateway.logger.error('configuration map.user missing mapTo definition for mandatory id/userName')
1485
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1486
+ }
1487
+ }
1488
+ if (!config?.map?.group) {
1489
+ scimgateway.logger.info('configuration map.group is not defiend and groups will not be supported')
1490
+ } else {
1491
+ let idFound = false
1492
+ let displayNameFound = false
1493
+ for (const key in config.map.group) {
1494
+ if (config.map.group[key].mapTo === 'id') idFound = true
1495
+ else if (config.map.group[key].mapTo === 'displayName') displayNameFound = true
1496
+ if (idFound && displayNameFound) break
1497
+ }
1498
+ if ((!idFound || !displayNameFound) && (Object.keys(config.map.group).length > 0)) {
1499
+ scimgateway.logger.error('configuration map.group missing mapTo definition for mandatory id/displayName')
1500
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1501
+ }
1502
+ }
1503
+
1504
+ for (const key in config.entity) {
1505
+ const userBase = config.entity[key]?.ldap?.userBase
1506
+ const groupBase = config.entity[key]?.ldap?.groupBase
1507
+ if (!userBase) {
1508
+ scimgateway.logger.error(`configuration missing mandatory endpoint.entity.${key}.ldap.userBase`)
1509
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1510
+ }
1511
+ if (!groupBase && config?.map?.group && Object.keys(config.map.group).length > 0) {
1512
+ scimgateway.logger.error(`configuration missing mandatory endpoint.entity.${key}.ldap.groupBase`)
1513
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1514
+ }
1515
+ let usrArr = config.entity[key]?.ldap?.namingAttribute?.user
1516
+ if (!usrArr || !Array.isArray(usrArr)) { // check for legacy
1517
+ const attr = config.entity[key]?.ldap?.userNamingAttr
1518
+ if (attr) {
1519
+ usrArr = [{ attribute: attr, mapTo: 'userName' }]
1520
+ if (!config.entity[key].ldap.namingAttribute) config.entity[key].ldap.namingAttribute = {}
1521
+ config.entity[key].ldap.namingAttribute.user = scimgateway.copyObj(usrArr)
1522
+ }
1523
+ }
1524
+ if (!Array.isArray(usrArr) || usrArr.length !== 1) {
1525
+ scimgateway.logger.error(`configuration missing namingAttribute: endpoint.entity.${key}.ldap.namingAttribute.user`)
1526
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1527
+ }
1528
+ if (!usrArr[0].attribute || !usrArr[0].mapTo) {
1529
+ scimgateway.logger.error(`configuration missing attribute/mapTo: endpoint.entity.${key}.ldap.namingAttribute.user`)
1530
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1531
+ }
1532
+ const [endpointAttr] = scimgateway.endpointMapper('outbound', usrArr[0].mapTo, config.map.user)
1533
+ if (!endpointAttr) {
1534
+ scimgateway.logger.error(`configuration namingAttribute mapTo:${usrArr[0].mapTo} cannot be found in the map user configuration`)
1535
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1536
+ }
1537
+
1538
+ let grpArr = config.entity[key]?.ldap?.namingAttribute?.group
1539
+ if (config?.map?.group && Object.keys(config.map.group).length > 0) {
1540
+ if (!grpArr || !Array.isArray(grpArr)) { // check for legacy
1541
+ const attr = config.entity[key]?.ldap?.groupNamingAttr
1542
+ if (attr) {
1543
+ grpArr = [{ attribute: attr, mapTo: 'displayName' }]
1544
+ if (!config.entity[key].ldap.namingAttribute) config.entity[key].ldap.namingAttribute = {}
1545
+ config.entity[key].ldap.namingAttribute.group = scimgateway.copyObj(grpArr)
1546
+ }
1547
+ }
1548
+ if (!Array.isArray(grpArr) || grpArr.length !== 1) {
1549
+ scimgateway.logger.error(`configuration missing namingAttribute: endpoint.entity.${key}.ldap.namingAttribute.group`)
1550
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1551
+ }
1552
+ if (!grpArr[0].attribute || !grpArr[0].mapTo) {
1553
+ scimgateway.logger.error(`configuration missing attribute/mapTo: endpoint.entity.${key}.ldap.namingAttribute.group`)
1554
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1555
+ }
1556
+ const [endpointAttr] = scimgateway.endpointMapper('outbound', grpArr[0].mapTo, config.map.group)
1557
+ if (!endpointAttr) {
1558
+ scimgateway.logger.error(`configuration namingAttribute mapTo:${grpArr[0].mapTo} cannot be found in the map group configuration`)
1559
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1560
+ }
1561
+ }
1562
+ }
1563
+
1564
+ config.useSID_id = config.map.user.objectSid && config.map.user.objectSid.mapTo === 'id' // AD proprietary SID/GUID
1565
+ config.useGUID_id = config.map.user.objectGUID && config.map.user.objectGUID.mapTo === 'id'
1566
+ if (config.useSID_id && config.map.group) {
1567
+ if (!config.map.group.objectSid || config.map.group.objectSid.mapTo !== 'id') {
1568
+ scimgateway.logger.error('configuration missing group.objectSid - user and group should be using the same attribute')
1569
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1570
+ }
1571
+ } else if (config.useGUID_id && config.map.group) {
1572
+ if (!config.map.group.objectGUID || config.map.group.objectGUID.mapTo !== 'id') {
1573
+ scimgateway.logger.error('configuration missing group.objectGUID - user and group should be using the same attribute')
1574
+ throw new Error(`using exception to exit ${pluginName}, please ignore message...`)
1575
+ }
1576
+ }
1577
+ if (config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) { // support mapping different inbound/outbound upn domain names
1578
+ if (config.map.user.userPrincipalName.mapDomain.inbound && config.map.user.userPrincipalName.mapDomain.outbound) {
1579
+ const inbound = config.map.user.userPrincipalName.mapDomain.inbound
1580
+ const outbound = config.map.user.userPrincipalName.mapDomain.outbound
1581
+ config.map.user.userPrincipalName.mapDomain.inbound = inbound.startsWith('@') ? inbound : '@' + inbound // "@my-company.com"
1582
+ config.map.user.userPrincipalName.mapDomain.outbound = outbound.startsWith('@') ? outbound : '@' + outbound // "@test.onmicrosoft.com
1583
+ } else delete config.map.user.userPrincipalName.mapDomain
1584
+ }
@@ -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/lib/utils.js CHANGED
@@ -330,7 +330,7 @@ module.exports.extendObjClear = (obj, src, isSoftSync) => {
330
330
  break
331
331
  }
332
332
  }
333
- if (!found) {
333
+ if (!found) {
334
334
  const v = module.exports.copyObj(val)
335
335
  if (!isSoftSync) v.operation = 'delete'
336
336
  addArr.push(v)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.5.7",
3
+ "version": "4.5.9",
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",
@@ -34,7 +34,7 @@
34
34
  "callsite": "^1.0.0",
35
35
  "dot-object": "^2.1.5",
36
36
  "fold-to-ascii": "^5.0.1",
37
- "https-proxy-agent": "^7.0.4",
37
+ "https-proxy-agent": "^7.0.5",
38
38
  "is-in-subnet": "^4.0.1",
39
39
  "jsonwebtoken": "^9.0.2",
40
40
  "koa": "^2.15.3",
@@ -42,14 +42,14 @@
42
42
  "koa-router": "^12.0.1",
43
43
  "ldapjs": "^3.0.7",
44
44
  "lokijs": "^1.5.12",
45
- "mongodb": "^6.6.2",
46
- "nats": "^2.26.0",
45
+ "mongodb": "^6.9.0",
46
+ "nats": "^2.28.2",
47
47
  "node-machine-id": "1.1.9",
48
- "nodemailer": "^6.9.13",
48
+ "nodemailer": "^6.9.15",
49
49
  "passport": "^0.7.0",
50
50
  "passport-azure-ad": "^4.3.5",
51
- "tedious": "^18.2.0",
52
- "winston": "^3.13.0"
51
+ "tedious": "^18.6.1",
52
+ "winston": "^3.14.2"
53
53
  },
54
54
  "devDependencies": {
55
55
  "chai": "^4.2.0",