scimgateway 4.4.6 → 4.5.0

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,6 +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 to the publisher. Using SCIM Stream we have `egress/outbound only traffic` and get loadbalancing/failover by adding more gateways subscribing to same channel.
19
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
20
21
  - Authentication PassThrough letting plugin pass authentication directly to endpoint for avoid maintaining secrets at the gateway. Kubernetes health checks and shutdown handler support
21
22
  - Supports OAuth Client Credentials authentication
@@ -33,7 +34,9 @@ Latest news:
33
34
 
34
35
  ## Overview
35
36
 
36
- With SCIM Gateway, user management is facilitated through the utilization of the REST-based SCIM 1.1 or 2.0 protocol. The Gateway acts as a translator for incoming SCIM requests, seamlessly enabling the exposure of CRUD functionality (create, read, update, and delete user/group) towards destinations. This is achieved through the implementation of endpoint-specific protocols, ensuring precise and efficient provisioning with diverse endpoints.
37
+ With SCIM Gateway, user management is facilitated through the utilization of the REST-based SCIM 1.1 or 2.0 protocol. The gateway acts as a translator for incoming SCIM requests, seamlessly enabling the exposure of CRUD functionality (create, read, update, and delete user/group) towards destinations. This is achieved through the implementation of endpoint-specific protocols, ensuring precise and efficient provisioning with diverse endpoints.
38
+
39
+ Using [SCIM Stream](https://elshaug.xyz/docs/scim-stream), gateway may enable Pub/Sub allowing incoming SCIM requests to be published and processed by other gateways acting as subscribers. This extends beyond being Entra ID and HR subscriber for messages published by the SCIM Stream collector.
37
40
 
38
41
  ![](https://jelhub.github.io/images/ScimGateway.svg)
39
42
 
@@ -54,10 +57,10 @@ Same as plugin "Loki", but using external MongoDB
54
57
  Shows how to implement a highly configurable multi tenant or multi endpoint solution through `baseEntity` in URL
55
58
 
56
59
  * **SCIM** (REST Webservice)
57
- Demonstrates user provisioning towards REST-Based endpoint (type SCIM)
58
- Using plugin "Loki" as SCIM endpoint
60
+ Demonstrates user provisioning towards REST-Based endpoint (type SCIM)
61
+ Using plugin Loki as SCIM endpoint
59
62
  Can be used as SCIM version-gateway e.g. 1.1=>2.0 or 2.0=>1.1
60
- Can be used to chain several SCIM Gateway's
63
+ Can be used to chain several gateways
61
64
 
62
65
 
63
66
  * **Soap** (SOAP Webservice)
@@ -86,9 +89,9 @@ Using endpointMapper (like plugin-entra-id) for attribute flexibility
86
89
  * **API** (REST Webservices)
87
90
  Demonstrates API Gateway/plugin functionality using post/put/patch/get/delete
88
91
  None SCIM plugin, becomes what you want it to become.
89
- Methods listed can also be used in standard SCIM plugins
92
+ Methods included can also be used in standard SCIM plugins
90
93
  Endpoint complexity could be put in this plugin, and client could instead communicate through Gateway using your own simplified REST specification.
91
- One example of usage could be creation of tickets in ServiceDesk/HelpDesk and also the other way, closing a ticket could automatically approve/reject corresponding workflow in Identity Manager.
94
+ One example of usage could be creation of tickets in ServiceDesk and also the other way, closing a ticket could automatically approve/reject corresponding workflow in IdP.
92
95
 
93
96
 
94
97
  ## Installation
@@ -384,13 +387,13 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
384
387
 
385
388
  - **auth.bearerJwtAzure** - Array of one or more JWT used by Azure SyncFabric. **tenantIdGUID** must be set to Entra ID Tenant ID.
386
389
 
387
- - **auth.bearerJwt** - Array of one or more standard JWT objects. Using **secret** or **publicKey** for signature verification. publicKey should be set to the filename of public key or certificate pem-file located in `<package-root>\config\certs`. Clear text secret will become encrypted when gateway is started. **options.issuer** is mandatory. Other options may also be included according to jsonwebtoken npm package definition.
390
+ - **auth.bearerJwt** - Array of one or more standard JWT objects. Using **secret** or **publicKey** for signature verification. publicKey should be set to the filename of public key or certificate pem-file located in `<package-root>\config\certs` or absolute path being used. Clear text secret will become encrypted when gateway is started. **options.issuer** is mandatory. Other options may also be included according to jsonwebtoken npm package definition.
388
391
 
389
392
  - **auth.bearerOAuth** - Array of one or more Client Credentials OAuth configuration objects. **`client_id`** and **`client_secret`** are mandatory. client_secret value will become encrypted when gateway is started. OAuth token request url is **/oauth/token** e.g. http://localhost:8880/oauth/token
390
393
 
391
394
  - **auth.passThrough** - Setting **auth.passThrough.enabled=true** will bypass SCIM Gateway authentication. Gateway will instead pass ctx containing authentication header to the plugin. Plugin could then use this information for endpoint authentication and we don't have any password/token stored at the gateway. Note, this also requires plugin binary having `scimgateway.authPassThroughAllowed = true` and endpoint logic for handling/passing ctx.request.header.authorization
392
395
 
393
- - **certificate** - If not using TLS certificate, set "key", "cert" and "ca" to **null**. When using TLS, "key" and "cert" have to be defined with the filename corresponding to the primary-key and public-certificate. Both files must be located in the `<package-root>\config\certs` directory e.g:
396
+ - **certificate** - If not using TLS certificate, set "key", "cert" and "ca" to **null**. When using TLS, "key" and "cert" have to be defined with the filename corresponding to the primary-key and public-certificate. Both files must be located in the `<package-root>\config\certs` directory unless absolute path being defined e.g:
394
397
 
395
398
  "certificate": {
396
399
  "key": "key.pem",
@@ -1147,6 +1150,16 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1147
1150
 
1148
1151
  ## Change log
1149
1152
 
1153
+ ### v4.5.0
1154
+
1155
+ [Improved]
1156
+
1157
+ - scim-stream, scimgateway now 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 to the publisher. Using SCIM Stream we have `egress/outbound only traffic` and get loadbalancing/failover by adding more gateways subscribing to same channel.
1158
+ - scim-stream, subscriber will do automatic retry until connected when plugin not able to connect to endpoint (offline endpoint)
1159
+ - plugin-ldap, modifyGroup now supports all attributes and not only add/remove members
1160
+ - certificate absolute path may be used in plugin configuration file instead of default relative path
1161
+ - dependencies bump
1162
+
1150
1163
  ### v4.4.6
1151
1164
 
1152
1165
  [Improved]
@@ -116,6 +116,19 @@
116
116
  "replaceDomains": []
117
117
  }
118
118
  }
119
+ },
120
+ "publisher": {
121
+ "enabled": false,
122
+ "entity": {
123
+ "undefined": {
124
+ "nats": {
125
+ "tenant": null,
126
+ "subject": null,
127
+ "jwt": null,
128
+ "secret": null
129
+ }
130
+ }
131
+ }
119
132
  }
120
133
  }
121
134
  },
@@ -116,6 +116,19 @@
116
116
  "replaceDomains": []
117
117
  }
118
118
  }
119
+ },
120
+ "publisher": {
121
+ "enabled": false,
122
+ "entity": {
123
+ "undefined": {
124
+ "nats": {
125
+ "tenant": null,
126
+ "subject": null,
127
+ "jwt": null,
128
+ "secret": null
129
+ }
130
+ }
131
+ }
119
132
  }
120
133
  }
121
134
  },
@@ -116,6 +116,19 @@
116
116
  "replaceDomains": []
117
117
  }
118
118
  }
119
+ },
120
+ "publisher": {
121
+ "enabled": false,
122
+ "entity": {
123
+ "undefined": {
124
+ "nats": {
125
+ "tenant": null,
126
+ "subject": null,
127
+ "jwt": null,
128
+ "secret": null
129
+ }
130
+ }
131
+ }
119
132
  }
120
133
  }
121
134
  },
@@ -116,6 +116,19 @@
116
116
  "replaceDomains": []
117
117
  }
118
118
  }
119
+ },
120
+ "publisher": {
121
+ "enabled": false,
122
+ "entity": {
123
+ "undefined": {
124
+ "nats": {
125
+ "tenant": null,
126
+ "subject": null,
127
+ "jwt": null,
128
+ "secret": null
129
+ }
130
+ }
131
+ }
119
132
  }
120
133
  }
121
134
  },
@@ -122,6 +122,19 @@
122
122
  "replaceDomains": []
123
123
  }
124
124
  }
125
+ },
126
+ "publisher": {
127
+ "enabled": false,
128
+ "entity": {
129
+ "undefined": {
130
+ "nats": {
131
+ "tenant": null,
132
+ "subject": null,
133
+ "jwt": null,
134
+ "secret": null
135
+ }
136
+ }
137
+ }
125
138
  }
126
139
  }
127
140
  },
@@ -116,6 +116,19 @@
116
116
  "replaceDomains": []
117
117
  }
118
118
  }
119
+ },
120
+ "publisher": {
121
+ "enabled": false,
122
+ "entity": {
123
+ "undefined": {
124
+ "nats": {
125
+ "tenant": null,
126
+ "subject": null,
127
+ "jwt": null,
128
+ "secret": null
129
+ }
130
+ }
131
+ }
119
132
  }
120
133
  }
121
134
  },
@@ -116,6 +116,19 @@
116
116
  "replaceDomains": []
117
117
  }
118
118
  }
119
+ },
120
+ "publisher": {
121
+ "enabled": false,
122
+ "entity": {
123
+ "undefined": {
124
+ "nats": {
125
+ "tenant": null,
126
+ "subject": null,
127
+ "jwt": null,
128
+ "secret": null
129
+ }
130
+ }
131
+ }
119
132
  }
120
133
  }
121
134
  },
@@ -116,6 +116,19 @@
116
116
  "replaceDomains": []
117
117
  }
118
118
  }
119
+ },
120
+ "publisher": {
121
+ "enabled": false,
122
+ "entity": {
123
+ "undefined": {
124
+ "nats": {
125
+ "tenant": null,
126
+ "subject": null,
127
+ "jwt": null,
128
+ "secret": null
129
+ }
130
+ }
131
+ }
119
132
  }
120
133
  }
121
134
  },
@@ -116,6 +116,19 @@
116
116
  "replaceDomains": []
117
117
  }
118
118
  }
119
+ },
120
+ "publisher": {
121
+ "enabled": false,
122
+ "entity": {
123
+ "undefined": {
124
+ "nats": {
125
+ "tenant": null,
126
+ "subject": null,
127
+ "jwt": null,
128
+ "secret": null
129
+ }
130
+ }
131
+ }
119
132
  }
120
133
  }
121
134
  },
package/lib/plugin-api.js CHANGED
@@ -253,6 +253,7 @@ const getAccessToken = async (baseEntity, ctx) => {
253
253
  grant_type: 'client_credentials',
254
254
  client_id: username || config.entity[baseEntity].oauth.clientId,
255
255
  client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.oauth.clientSecret`, configFile),
256
+ scope: config.entity[baseEntity].oauth.scope || '',
256
257
  resource: resource // "https://graph.microsoft.com"
257
258
  }
258
259
  options.headers = {
@@ -1069,6 +1069,7 @@ const getAccessToken = async (baseEntity, ctx) => {
1069
1069
  grant_type: 'client_credentials',
1070
1070
  client_id: username || config.entity[baseEntity].oauth.clientId,
1071
1071
  client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.oauth.clientSecret`, configFile),
1072
+ scope: config.entity[baseEntity].oauth.scope || '',
1072
1073
  resource: resource // "https://graph.microsoft.com"
1073
1074
  }
1074
1075
  options.headers = {
@@ -289,8 +289,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
289
289
  if (!userBase) userBase = config.entity[baseEntity].ldap.userBase
290
290
 
291
291
  // convert SCIM attributes to endpoint attributes according to config.map
292
- const [endpointObj] = scimgateway.endpointMapper('outbound', userObj, config.map.user)
293
- // if (err) throw new Error(`${action} error: ${err.message}`) // use above [endpointObj, err] to catch non supported attributes
292
+ const [endpointObj] = scimgateway.endpointMapper('outbound', userObj, config.map.user) // use [endpointObj, err] and if err, throw error to catch non supported attributes
294
293
 
295
294
  // endpoint spesific attribute handling
296
295
  if (endpointObj.sAMAccountName !== undefined) { // Active Directory
@@ -372,18 +371,16 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
372
371
  delete attrObj.groups // make sure to be removed from attrObj
373
372
 
374
373
  const [groupsAttr] = scimgateway.endpointMapper('outbound', 'groups.value', config.map.user)
375
- // if (err) throw new Error(`${action} error: ${err.message}`) // use above [endpointObj, err] to catch non supported attributes
376
-
377
- const body = { add: { }, remove: { } }
378
- body.add[groupsAttr] = []
379
- body.remove[groupsAttr] = []
374
+ const grp = { add: { }, remove: { } }
375
+ grp.add[groupsAttr] = []
376
+ grp.remove[groupsAttr] = []
380
377
 
381
378
  for (let i = 0; i < groups.length; i++) {
382
379
  const el = groups[i]
383
380
  if (el.operation && el.operation === 'delete') { // delete from users group attribute
384
- body.remove[groupsAttr].push(el.value)
381
+ grp.remove[groupsAttr].push(el.value)
385
382
  } else { // add to users group attribute
386
- body.add[groupsAttr].push(el.value)
383
+ grp.add[groupsAttr].push(el.value)
387
384
  }
388
385
  }
389
386
 
@@ -399,17 +396,17 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
399
396
  } else base = id // dn
400
397
 
401
398
  try {
402
- if (body.add[groupsAttr].length > 0) {
399
+ if (grp.add[groupsAttr].length > 0) {
403
400
  const ldapOptions = {
404
401
  operation: 'add',
405
- modification: body.add
402
+ modification: grp.add
406
403
  }
407
404
  await doRequest(baseEntity, method, base, ldapOptions, ctx)
408
405
  }
409
- if (body.remove[groupsAttr].length > 0) {
406
+ if (grp.remove[groupsAttr].length > 0) {
410
407
  const ldapOptions = {
411
408
  operation: 'delete',
412
- modification: body.remove
409
+ modification: grp.remove
413
410
  }
414
411
  await doRequest(baseEntity, method, base, ldapOptions, ctx)
415
412
  }
@@ -418,11 +415,10 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
418
415
  }
419
416
  }
420
417
 
421
- if (JSON.stringify(attrObj) === '{}') return null // only groups included
418
+ if (Object.keys(attrObj).length < 1) return null // only groups included
422
419
 
423
420
  // convert SCIM attributes to endpoint attributes according to config.map
424
421
  const [endpointObj] = scimgateway.endpointMapper('outbound', attrObj, config.map.user)
425
- // if (err) throw new Error(`${action} error: ${err.message}`) // use above [endpointObj, err] to catch non supported attributes
426
422
 
427
423
  // endpoint spesific attribute handling
428
424
  if (endpointObj.userAccountControl !== undefined) { // SCIM "active" - Active Directory
@@ -527,7 +523,6 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
527
523
  }
528
524
 
529
525
  const [attrs] = scimgateway.endpointMapper('outbound', attributes, config.map.group) // SCIM/CustomSCIM => endpoint attribute naming
530
-
531
526
  const method = 'search'
532
527
  const scope = 'sub'
533
528
  let base = config.entity[baseEntity].ldap.groupBase
@@ -644,8 +639,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
644
639
  if (!config.map.group) throw new Error(`${action} error: missing configuration endpoint.map.group`)
645
640
 
646
641
  // convert SCIM attributes to endpoint attributes according to config.map
647
- const [endpointObj, err] = scimgateway.endpointMapper('outbound', groupObj, config.map.group)
648
- if (err) throw new Error(`${action} error: ${err.message}`)
642
+ const [endpointObj] = scimgateway.endpointMapper('outbound', groupObj, config.map.group)
649
643
 
650
644
  // endpointObj.objectClass is mandatory and must must match your ldap schema
651
645
  endpointObj.objectClass = config.entity[baseEntity].ldap.groupObjectClasses // Active Directory: ["group"]
@@ -700,21 +694,18 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
700
694
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
701
695
 
702
696
  if (!config.map.group) throw new Error(`${action} error: missing configuration endpoint.map.group`)
703
- if (!attrObj.members) {
704
- throw new Error(`${action} error: only supports modification of members`)
705
- }
706
- if (!Array.isArray(attrObj.members)) {
697
+ if (attrObj.members && !Array.isArray(attrObj.members)) {
707
698
  throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
708
699
  }
709
700
 
710
- const [memberAttr, err1] = scimgateway.endpointMapper('outbound', 'members.value', config.map.group)
711
- if (err1) throw new Error(`${action} error: ${err1.message}`)
701
+ const [memberAttr] = scimgateway.endpointMapper('outbound', 'members.value', config.map.group)
702
+ if (!memberAttr && attrObj.members) throw new Error(`${action} error: missing attribute mapping configuration for group members`)
712
703
 
713
- const body = { add: { }, remove: { } }
714
- body.add[memberAttr] = []
715
- body.remove[memberAttr] = []
704
+ const grp = { add: { }, remove: { } }
705
+ grp.add[memberAttr] = []
706
+ grp.remove[memberAttr] = []
716
707
 
717
- for (let i = 0; i < attrObj.members.length; i++) {
708
+ for (let i = 0; i < attrObj?.members?.length; i++) {
718
709
  const el = attrObj.members[i]
719
710
  if (config.useSID_id || config.useGUID_id) {
720
711
  const dn = await sidGuidToDn(baseEntity, el.value, ctx)
@@ -722,9 +713,9 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
722
713
  el.value = dn
723
714
  }
724
715
  if (el.operation && el.operation === 'delete') { // delete member from group
725
- body.remove[memberAttr].push(el.value) // endpointMapper returns URI encoded id because some IdP's don't encode id used in GET url e.g. Symantec/Broadcom/CA
716
+ grp.remove[memberAttr].push(el.value) // endpointMapper returns URI encoded id because some IdP's don't encode id used in GET url e.g. Symantec/Broadcom/CA
726
717
  } else { // add member to group
727
- body.add[memberAttr].push(el.value)
718
+ grp.add[memberAttr].push(el.value)
728
719
  }
729
720
  }
730
721
 
@@ -735,17 +726,26 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
735
726
  else base = id // dn
736
727
 
737
728
  try {
738
- if (body.add[memberAttr].length > 0) {
729
+ delete attrObj.members
730
+ const [endpointObj] = scimgateway.endpointMapper('outbound', attrObj, config.map.group)
731
+ if (Object.keys(endpointObj).length > 0) {
732
+ const ldapOptions = {
733
+ operation: 'replace',
734
+ modification: endpointObj
735
+ }
736
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
737
+ }
738
+ if (grp.add[memberAttr].length > 0) {
739
739
  const ldapOptions = { // using ldap lookup (dn) instead of search
740
740
  operation: 'add',
741
- modification: body.add
741
+ modification: grp.add
742
742
  }
743
743
  await doRequest(baseEntity, method, base, ldapOptions, ctx)
744
744
  }
745
- if (body.remove[memberAttr].length > 0) {
745
+ if (grp.remove[memberAttr].length > 0) {
746
746
  const ldapOptions = { // using ldap lookup (dn) instead of search
747
747
  operation: 'delete',
748
- modification: body.remove
748
+ modification: grp.remove
749
749
  }
750
750
  await doRequest(baseEntity, method, base, ldapOptions, ctx)
751
751
  }
@@ -51,7 +51,7 @@ const validFilterOperators = ['eq', 'ne', 'aeq', 'dteq', 'gt', 'gte', 'lt', 'lte
51
51
 
52
52
  const dbNames = []
53
53
  for (const baseEntity in config.entity) {
54
- let dbname = (config.entity[baseEntity].dbname ? config.entity[baseEntity].dbname : 'loki.db')
54
+ let dbname = config.entity[baseEntity].dbname || 'loki.db'
55
55
  if (dbNames.includes(dbname)) {
56
56
  scimgateway.logger.error(`${pluginName}[${baseEntity}] initialization error: database '${dbname}' is already used by another baseEntity configuration`)
57
57
  continue
@@ -587,6 +587,7 @@ const getAccessToken = async (baseEntity, ctx) => {
587
587
  grant_type: 'client_credentials',
588
588
  client_id: username || config.entity[baseEntity].oauth.clientId,
589
589
  client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.oauth.clientSecret`, configFile),
590
+ scope: config.entity[baseEntity].oauth.scope || '',
590
591
  resource: resource // "https://graph.microsoft.com"
591
592
  }
592
593
  options.headers = {