scimgateway 4.1.15 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,9 +16,10 @@ Validated through IdP's:
16
16
 
17
17
  Latest news:
18
18
 
19
+ - Authentication PassThrough letting plugin pass authentication directly to endpoint for avoid maintaining secrets at the gateway. Kubernetes health checks and shutdown handler support
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 Stream includes **SCIM Stream Gateway**, the next generation SCIM Gateway that supports message subscription and automated provisioning
20
21
  - Supports OAuth Client Credentials authentication
21
- - Major version v4.0.0. getUsers() and getGroups() replacing some deprecated methods. No limitations on filtering/sorting. Admin user access can be linked to specific baseEntities. New MongoDB plugin
22
+ - Major version v4.0.0. getUsers() and getGroups() replacing some deprecated methods. No limitations on filtering/sorting. Admin user access can be linked to specific baseEntities. New MongoDB plugin
22
23
  - ipAllowList for restricting access to allowlisted IP addresses or subnets e.g. Azure AD IP-range
23
24
  - General LDAP plugin configured for Active Directory
24
25
  - [PlugSSO](https://elshaug.xyz/docs/plugsso) using SCIM Gateway
@@ -291,7 +292,12 @@ Below shows an example of config\plugin-saphana.json
291
292
  "to": null,
292
293
  "cc": null
293
294
  }
294
- }
295
+ },
296
+ "kubernetes": {
297
+ "enabled": false,
298
+ "shutdownTimeout": 15000,
299
+ "forceExitTimeout": 1000
300
+ }
295
301
  },
296
302
  "endpoint": {
297
303
  "host": "hostname",
@@ -305,7 +311,7 @@ Below shows an example of config\plugin-saphana.json
305
311
 
306
312
  Configuration file have two main JSON objects: `scimgateway` and `endpoint`
307
313
 
308
- Definitions in `scimgateway` object have fixed attributes, but values can be modified. This object is used by the core functionality of the SCIM Gateway.
314
+ Definitions in `scimgateway` object have fixed attributes, but values can be modified. Sections not used/configured can be removed. This object is used by the core functionality of the SCIM Gateway.
309
315
 
310
316
  Definitions in `endpoint` object are customized according to our plugin code. Plugin typically need this information for communicating with endpoint
311
317
 
@@ -408,6 +414,11 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
408
414
  - **emailOnError.smtp.to** - Comma separated list of recipients email addresses e.g: "someone@example.com"
409
415
  - **emailOnError.smtp.cc** - Comma separated list of cc email addresses
410
416
 
417
+ - **kubernetes** - Enable Kubernetes support for healthchecks and graceful shutdown.
418
+ - **kubernetes.enabled** - true or false, true will enable Kubernets health checks and shutdown handler
419
+ - **kubernetes.shutdownTimeout** - Number of milliseconds to wait before shutting down (default 15000).
420
+ - **kubernetes.forceExitTimeout** - Number of milliseconds before forceful exiting (default 1000).
421
+
411
422
  - **endpoint** - Contains endpoint specific configuration according to our **plugin code**.
412
423
 
413
424
  #### Configuration notes
@@ -1040,12 +1051,13 @@ Plugins should have following initialization:
1040
1051
  let validScimAttr = [] // empty array - all attrbutes are supported by endpoint
1041
1052
  // add any external config process.env and process.file
1042
1053
  config = scimgateway.processExtConfig(pluginName, config)
1054
+ scimgateway.authPassThroughAllowed = false
1043
1055
  // mandatory plugin initialization - end
1044
1056
 
1045
1057
 
1046
1058
  ### getUsers
1047
1059
 
1048
- scimgateway.getUsers = async (baseEntity, getObj, attributes) => {
1060
+ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
1049
1061
  let ret = {
1050
1062
  "Resources": [],
1051
1063
  "totalResults": null
@@ -1067,7 +1079,7 @@ ret.totalResults = if supporting pagination, then it should be set to the total
1067
1079
 
1068
1080
  ### deleteUser
1069
1081
 
1070
- scimgateway.deleteUser = async (baseEntity, id) => {
1082
+ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
1071
1083
  ...
1072
1084
  return null
1073
1085
  }
@@ -1077,7 +1089,7 @@ ret.totalResults = if supporting pagination, then it should be set to the total
1077
1089
 
1078
1090
  ### modifyUser
1079
1091
 
1080
- scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
1092
+ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
1081
1093
  ...
1082
1094
  return null
1083
1095
  }
@@ -1090,7 +1102,7 @@ Note, multi-value attributes excluding user attribute 'groups' are customized fr
1090
1102
 
1091
1103
  ### getGroups
1092
1104
 
1093
- scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
1105
+ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
1094
1106
  let ret = {
1095
1107
  "Resources": [],
1096
1108
  "totalResults": null
@@ -1112,7 +1124,7 @@ ret.totalResults = if supporting pagination, then it should be set to the total
1112
1124
 
1113
1125
 
1114
1126
  ### createGroup
1115
- scimgateway.createGroup = async (baseEntity, groupObj) => {
1127
+ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
1116
1128
  ...
1117
1129
  return null
1118
1130
  })
@@ -1122,7 +1134,7 @@ groupObj.displayName contains the group name to be created
1122
1134
  * return null: null if OK, else throw error
1123
1135
 
1124
1136
  ### deleteGroup
1125
- scimgateway.deleteGroup = async (baseEntity, id) => {
1137
+ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
1126
1138
  ...
1127
1139
  return null
1128
1140
  }
@@ -1132,7 +1144,7 @@ groupObj.displayName contains the group name to be created
1132
1144
 
1133
1145
  ### modifyGroup
1134
1146
 
1135
- scimgateway.modifyGroup = async (baseEntity, id, attrObj) => {
1147
+ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
1136
1148
  ...
1137
1149
  return null
1138
1150
  }
@@ -1153,11 +1165,35 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1153
1165
 
1154
1166
  ## Change log
1155
1167
 
1168
+ ### v4.2.1
1169
+
1170
+ [Fixed]
1171
+
1172
+ - plugin-azure-ad createUser failed when manager was included
1173
+ - plugin-ldap slow when not using group/groupBase configuration
1174
+
1175
+
1176
+ ### v4.2.0
1177
+
1178
+ [Added]
1179
+
1180
+ - Kubernetes health checks and shutdown handler support
1181
+
1182
+ Plugin configuration prerequisite: **kubernetes.enabled=true**
1183
+
1184
+ "kubernetes": {
1185
+ "enabled": true,
1186
+ "shutdownTimeout": 15000,
1187
+ "forceExitTimeout": 1000
1188
+ }
1189
+
1190
+ **Thanks to Kevin Osborn**
1191
+
1156
1192
  ### v4.1.15
1157
1193
 
1158
1194
  [Added]
1159
1195
 
1160
- - authPassThrough for passing the authentication directly to plugin without being processed by scimgateway
1196
+ - Authentication PassThrough for passing the authentication directly to plugin without being processed by scimgateway. Plugin can then pass this authentication to endpoint for avoid maintaining secrets at the gateway.
1161
1197
 
1162
1198
  Plugin configuration prerequisites: **auth.passThrough.enabled=true**
1163
1199
 
@@ -1182,6 +1218,7 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1182
1218
  scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx)
1183
1219
  // tip, see provided example plugins
1184
1220
 
1221
+ **Thanks to Kevin Osborn**
1185
1222
 
1186
1223
  ### v4.1.14
1187
1224
 
@@ -87,6 +87,11 @@
87
87
  "to": null,
88
88
  "cc": null
89
89
  }
90
+ },
91
+ "kubernetes": {
92
+ "enabled": false,
93
+ "shutdownTimeout": 15000,
94
+ "forceExitTimeout": 1000
90
95
  }
91
96
  },
92
97
  "endpoint": {
@@ -87,6 +87,11 @@
87
87
  "to": null,
88
88
  "cc": null
89
89
  }
90
+ },
91
+ "kubernetes": {
92
+ "enabled": false,
93
+ "shutdownTimeout": 15000,
94
+ "forceExitTimeout": 1000
90
95
  }
91
96
  },
92
97
  "endpoint": {
@@ -87,6 +87,11 @@
87
87
  "to": null,
88
88
  "cc": null
89
89
  }
90
+ },
91
+ "kubernetes": {
92
+ "enabled": false,
93
+ "shutdownTimeout": 15000,
94
+ "forceExitTimeout": 1000
90
95
  }
91
96
  },
92
97
  "endpoint": {
@@ -87,6 +87,11 @@
87
87
  "to": null,
88
88
  "cc": null
89
89
  }
90
+ },
91
+ "kubernetes": {
92
+ "enabled": false,
93
+ "shutdownTimeout": 15000,
94
+ "forceExitTimeout": 1000
90
95
  }
91
96
  },
92
97
  "endpoint": {
@@ -87,6 +87,11 @@
87
87
  "to": null,
88
88
  "cc": null
89
89
  }
90
+ },
91
+ "kubernetes": {
92
+ "enabled": false,
93
+ "shutdownTimeout": 15000,
94
+ "forceExitTimeout": 1000
90
95
  }
91
96
  },
92
97
  "endpoint": {
@@ -93,6 +93,11 @@
93
93
  "to": null,
94
94
  "cc": null
95
95
  }
96
+ },
97
+ "kubernetes": {
98
+ "enabled": false,
99
+ "shutdownTimeout": 15000,
100
+ "forceExitTimeout": 1000
96
101
  }
97
102
  },
98
103
  "endpoint": {
@@ -87,6 +87,11 @@
87
87
  "to": null,
88
88
  "cc": null
89
89
  }
90
+ },
91
+ "kubernetes": {
92
+ "enabled": false,
93
+ "shutdownTimeout": 15000,
94
+ "forceExitTimeout": 1000
90
95
  }
91
96
  },
92
97
  "endpoint": {
@@ -87,6 +87,11 @@
87
87
  "to": null,
88
88
  "cc": null
89
89
  }
90
+ },
91
+ "kubernetes": {
92
+ "enabled": false,
93
+ "shutdownTimeout": 15000,
94
+ "forceExitTimeout": 1000
90
95
  }
91
96
  },
92
97
  "endpoint": {
@@ -87,6 +87,11 @@
87
87
  "to": null,
88
88
  "cc": null
89
89
  }
90
+ },
91
+ "kubernetes": {
92
+ "enabled": false,
93
+ "shutdownTimeout": 15000,
94
+ "forceExitTimeout": 1000
90
95
  }
91
96
  },
92
97
  "endpoint": {
@@ -243,11 +243,15 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
243
243
  const action = 'createUser'
244
244
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" userObj=${JSON.stringify(userObj)}`)
245
245
 
246
- const attrObj = {}
246
+ const addonObj = {}
247
247
  if (userObj.servicePlan) {
248
- attrObj.servicePlan = userObj.servicePlan // will be included in a modifyuser
248
+ addonObj.servicePlan = userObj.servicePlan
249
249
  delete userObj.servicePlan
250
250
  }
251
+ if (userObj.manager) {
252
+ addonObj.manager = userObj.manager
253
+ delete userObj.manager
254
+ }
251
255
 
252
256
  const method = 'POST'
253
257
  const path = '/users'
@@ -255,8 +259,8 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
255
259
 
256
260
  try {
257
261
  await doRequest(baseEntity, method, path, body)
258
- if (attrObj.servicePlan) {
259
- await scimgateway.modifyUser(baseEntity, userObj.userName, attrObj, ctx)
262
+ if (Object.keys(addonObj).length > 0) {
263
+ await scimgateway.modifyUser(baseEntity, userObj.userName, addonObj, ctx) // manager, servicePlan
260
264
  return null
261
265
  } else return (null)
262
266
  } catch (err) {
@@ -240,7 +240,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
240
240
 
241
241
  if (user.memberOf) {
242
242
  if (!config.map.group) user.memberOf = [] // empty any values
243
- else if (config.useSID_id || config.useGUID_id) { // Active Directory - convert memberOf having dn values to objectSid/objectGUID
243
+ if (config.useSID_id || config.useGUID_id) { // Active Directory - convert memberOf having dn values to objectSid/objectGUID
244
244
  const arr = []
245
245
  try {
246
246
  if (Array.isArray(user.memberOf)) {
@@ -261,7 +261,9 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
261
261
  }
262
262
  }
263
263
 
264
- return scimgateway.endpointMapper('inbound', user, config.map.user)[0] // endpoint attribute naming => SCIM
264
+ const scimObj = scimgateway.endpointMapper('inbound', user, config.map.user)[0] // endpoint attribute naming => SCIM
265
+ if (!scimObj.groups) scimObj.groups = []
266
+ return scimObj
265
267
  }))
266
268
  } catch (err) {
267
269
  throw new Error(`${action} error: ${err.message}`)
@@ -509,8 +511,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
509
511
  totalResults: null
510
512
  }
511
513
 
512
- if (!config.map.group) { // not using groups
513
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] "${action}" stopped - missing configuration endpoint.map.group`)
514
+ if (!config.map.group || !config.entity[baseEntity].ldap.groupBase) { // not using groups
515
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] "${action}" stopped - missing configuration endpoint.map.group or groupBase`)
514
516
  return result
515
517
  }
516
518
 
@@ -26,6 +26,7 @@ const callsite = require('callsite')
26
26
  const utils = require('../lib/utils')
27
27
  const countries = require('../lib/countries')
28
28
  const { createChecker } = require('is-in-subnet')
29
+ const { createTerminus } = require('@godaddy/terminus')
29
30
  require('events').EventEmitter.prototype._maxListeners = Infinity
30
31
 
31
32
  /**
@@ -82,6 +83,7 @@ const ScimGateway = function () {
82
83
  if (!config.certificate.pfx) config.certificate.pfx = {}
83
84
  if (!config.emailOnError) config.emailOnError = {}
84
85
  if (!config.emailOnError.smtp) config.emailOnError.smtp = {}
86
+ if (!config.kubernetes) config.kubernetes = {}
85
87
 
86
88
  if (config.ipAllowList && Array.isArray(config.ipAllowList) && config.ipAllowList.length > 0) {
87
89
  ipAllowListChecker = createChecker(config.ipAllowList)
@@ -491,7 +493,7 @@ const ScimGateway = function () {
491
493
  const obj = config.auth.passThrough
492
494
  if (obj.baseEntities) {
493
495
  if (Array.isArray(obj.baseEntities) && obj.baseEntities.length > 0) {
494
- if (!baseEntity || !obj.baseEntities.includes(baseEntity)) throw new Error(`baseEntity=${baseEntity} not allowed for passTrhough according to passTrhough configuration baseEntitites=${obj.baseEntities}`)
496
+ if (!baseEntity || !obj.baseEntities.includes(baseEntity)) throw new Error(`baseEntity=${baseEntity} not allowed for passThrough according to passThrough configuration baseEntitites=${obj.baseEntities}`)
495
497
  }
496
498
  }
497
499
  if (obj.readOnly === true && method !== 'GET') throw new Error('only allowing readOnly for passThrough according to passThrough configuration readOnly=true')
@@ -1768,6 +1770,45 @@ const ScimGateway = function () {
1768
1770
  }
1769
1771
  }
1770
1772
 
1773
+ function onSignal () {
1774
+ logger.info('server is starting cleanup')
1775
+ return Promise.all([
1776
+ // your clean logic, like closing database connections
1777
+ ])
1778
+ }
1779
+
1780
+ function onShutdown () {
1781
+ logger.info('cleanup finished, server is shutting down')
1782
+ }
1783
+
1784
+ function beforeShutdown () {
1785
+ return new Promise(resolve => {
1786
+ setTimeout(resolve, config.kubernetes.shutdownTimeout || 15000)
1787
+ })
1788
+ }
1789
+
1790
+ function healthCheck () {
1791
+ return Promise.resolve(
1792
+ // optionally include a resolve value to be included as
1793
+ // info in the health check response
1794
+ )
1795
+ }
1796
+ const options = {
1797
+ // health check options
1798
+ healthChecks: {
1799
+ '/healthcheck': healthCheck, // a function returning a promise indicating service health,
1800
+ verbatim: true // [optional = false] use object returned from /healthcheck verbatim in response
1801
+ },
1802
+
1803
+ // cleanup options
1804
+ timeout: config.kubernetes.forceExitTimeout || 1000, // [optional = 1000] number of milliseconds before forceful exiting
1805
+ beforeShutdown, // [optional] called before the HTTP server starts its shutdown
1806
+ onSignal, // [optional] cleanup function, returning a promise (used to be onSigterm)
1807
+ onShutdown // [optional] called right before exiting
1808
+ }
1809
+
1810
+ if (config.kubernetes.enabled) createTerminus(server, options)
1811
+
1771
1812
  // set loglevel according to config
1772
1813
  const arrValidLevel = ['silly', 'debug', 'verbose', 'info', 'warn', 'error']
1773
1814
  for (let i = 0; i < logger.transports.length; i++) {
@@ -2360,7 +2401,7 @@ ScimGateway.prototype.endpointMapper = function endpointMapper (direction, parse
2360
2401
  else dotKey = `${dotPath}.${key}`
2361
2402
  if (direction === 'outbound') { // outbound
2362
2403
  if (obj[key] === '') obj[key] = null
2363
- if (dotMap[dotKey] && dotMap[`${dotKey}.type`]) {
2404
+ if (dotMap[`${dotKey}.type`]) {
2364
2405
  const type = dotMap[`${dotKey}.type`].toLowerCase()
2365
2406
  if (type === 'boolean' && obj[key].constructor === String) {
2366
2407
  if ((obj[key]).toLowerCase() === 'true') obj[key] = true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.1.15",
3
+ "version": "4.2.1",
4
4
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
5
5
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
6
6
  "homepage": "https://elshaug.xyz",
@@ -32,6 +32,7 @@
32
32
  "node": ">=7.6.0"
33
33
  },
34
34
  "dependencies": {
35
+ "@godaddy/terminus": "^4.11.2",
35
36
  "callsite": "^1.0.0",
36
37
  "dot-object": "^2.1.4",
37
38
  "https-proxy-agent": "^5.0.1",