scimgateway 4.3.0 → 4.4.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/lib/plugin-api.js CHANGED
@@ -33,7 +33,6 @@ const URL = require('url').URL
33
33
  const querystring = require('querystring')
34
34
 
35
35
  // mandatory plugin initialization - start
36
- const path = require('path')
37
36
  let ScimGateway = null
38
37
  try {
39
38
  ScimGateway = require('scimgateway')
@@ -41,15 +40,16 @@ try {
41
40
  ScimGateway = require('./scimgateway')
42
41
  }
43
42
  const scimgateway = new ScimGateway()
44
- const pluginName = path.basename(__filename, '.js')
45
- const configDir = path.join(__dirname, '..', 'config')
46
- const configFile = path.join(`${configDir}`, `${pluginName}.json`)
43
+ const pluginName = scimgateway.pluginName
44
+ // const configDir = scimgateway.configDir
45
+ const configFile = scimgateway.configFile
47
46
  let config = require(configFile).endpoint
48
47
  config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file
49
48
  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
50
49
  // mandatory plugin initialization - end
51
50
 
52
51
  const _serviceClient = {}
52
+ const lock = new scimgateway.Lock()
53
53
 
54
54
  // =================================================
55
55
  // postApi
@@ -213,6 +213,10 @@ scimgateway.deleteApi = async (baseEntity, id, ctx) => {
213
213
  // helpers
214
214
  // =================================================
215
215
 
216
+ //
217
+ // start - REST endpoint template
218
+ //
219
+
216
220
  const getClientIdentifier = (ctx) => {
217
221
  if (!ctx?.request?.header?.authorization) return undefined
218
222
  const [user, secret] = getCtxAuth(ctx)
@@ -222,7 +226,7 @@ const getClientIdentifier = (ctx) => {
222
226
  //
223
227
  // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
224
228
  //
225
- const getCtxAuth = (ctx) => { // eslint-disable-line
229
+ const getCtxAuth = (ctx) => {
226
230
  if (!ctx?.request?.header?.authorization) return []
227
231
  const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
228
232
  let username, password
@@ -231,6 +235,85 @@ const getCtxAuth = (ctx) => { // eslint-disable-line
231
235
  else return [undefined, authToken] // bearer auth
232
236
  }
233
237
 
238
+ //
239
+ // getAccessToken - returns oauth accesstoken
240
+ //
241
+ const getAccessToken = async (baseEntity, ctx) => {
242
+ await lock.acquire()
243
+ const clientIdentifier = getClientIdentifier(ctx)
244
+ const d = new Date() / 1000 // seconds (unix time)
245
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken &&
246
+ (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
247
+ lock.release()
248
+ return _serviceClient[baseEntity][clientIdentifier].accessToken
249
+ }
250
+
251
+ const action = 'getAccessToken'
252
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Retrieving accesstoken`)
253
+
254
+ const method = 'POST'
255
+ const [, secret] = getCtxAuth(ctx) // if Auth PassTrough, secret from basic or bearer auth
256
+ let tokenUrl
257
+ let form
258
+
259
+ if (config.entity[baseEntity].oauth) {
260
+ let resource
261
+ try {
262
+ const urlObj = new URL(config.entity[baseEntity].baseUrls[0])
263
+ resource = urlObj.origin
264
+ } catch (err) {
265
+ resource = null
266
+ }
267
+ if (config.entity[baseEntity].oauth.tenantIdGUID) { // Azure
268
+ tokenUrl = `https://login.microsoftonline.com/${config.entity[baseEntity].oauth.tenantIdGUID}/oauth2/token`
269
+ } else {
270
+ tokenUrl = `https://login.microsoftonline.com/${config.entity[baseEntity].oauth.tokenUrl}`
271
+ }
272
+ form = {
273
+ grant_type: 'client_credentials',
274
+ client_id: config.entity[baseEntity].oauth.clientId,
275
+ client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.oauth.clientSecret`, configFile), // using config if no Auth PassThrough
276
+ resource: resource // "https://graph.microsoft.com"
277
+ }
278
+ } else {
279
+ const err = new Error(`[${action}] missing supported endpoint authentication configuration`)
280
+ lock.release()
281
+ throw (err)
282
+ }
283
+
284
+ const options = {
285
+ headers: {
286
+ 'Content-Type': 'application/x-www-form-urlencoded' // body must be query string formatted (no JSON)
287
+ }
288
+ }
289
+
290
+ try {
291
+ const response = await doRequest(baseEntity, method, tokenUrl, form, ctx, options)
292
+ if (!response.body) {
293
+ const err = new Error(`[${action}] No data retrieved from: ${method} ${tokenUrl}`)
294
+ throw (err)
295
+ }
296
+ const jbody = response.body
297
+ if (jbody.error) {
298
+ const err = new Error(`[${action}] Error message: ${jbody.error_description}`)
299
+ throw (err)
300
+ } else if (!jbody.access_token || !jbody.expires_in) {
301
+ const err = new Error(`[${action}] Error message: Retrieved invalid token response`)
302
+ throw (err)
303
+ }
304
+
305
+ const d = new Date() / 1000 // seconds (unix time)
306
+ jbody.validTo = d + parseInt(jbody.expires_in) // instead of using expires_on (clock may not be in sync with NTP, AAD default expires_in = 3600 seconds)
307
+ scimgateway.logger.silly(`${pluginName}[${baseEntity}] ${action}: AccessToken = ${jbody.access_token}`)
308
+
309
+ lock.release()
310
+ return jbody
311
+ } catch (err) {
312
+ lock.release()
313
+ throw (err)
314
+ }
315
+ }
316
+
234
317
  //
235
318
  // getServiceClient - returns options needed for connection parameters
236
319
  //
@@ -243,6 +326,11 @@ const getCtxAuth = (ctx) => { // eslint-disable-line
243
326
  const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
244
327
  const action = 'getServiceClient'
245
328
 
329
+ let authType
330
+ if (config.entity[baseEntity].basicAuth) authType = 'basicAuth'
331
+ else if (config.entity[baseEntity].oauth) authType = 'oauth'
332
+ else if (config.entity[baseEntity].bearerAuth) authType = 'bearerAuth'
333
+
246
334
  let urlObj
247
335
  if (!path) path = ''
248
336
  try {
@@ -251,17 +339,38 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
251
339
  //
252
340
  // path (no url) - default approach and client will be cached based on config
253
341
  //
342
+
254
343
  const clientIdentifier = getClientIdentifier(ctx)
255
- if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist
344
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist - Azure plugin specific
256
345
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using existing client`)
346
+ if (_serviceClient[baseEntity][clientIdentifier].accessToken) {
347
+ // check if token refresh is needed when using oauth
348
+ const d = new Date() / 1000 // seconds (unix time)
349
+ if (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
350
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Accesstoken about to expire in ${_serviceClient[baseEntity][clientIdentifier].accessToken.validTo - d} seconds`)
351
+ try {
352
+ const accessToken = await getAccessToken(baseEntity, ctx)
353
+ _serviceClient[baseEntity][clientIdentifier].accessToken = accessToken
354
+ _serviceClient[baseEntity][clientIdentifier].options.headers.Authorization = ` Bearer ${accessToken.access_token}`
355
+ } catch (err) {
356
+ delete _serviceClient[baseEntity][clientIdentifier]
357
+ const newErr = err
358
+ throw newErr
359
+ }
360
+ }
361
+ }
257
362
  } else {
258
363
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Client have to be created`)
259
364
  let client = null
260
365
  if (config.entity && config.entity[baseEntity]) client = config.entity[baseEntity]
261
366
  if (!client) {
262
- throw new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`)
367
+ const err = new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`)
368
+ throw err
369
+ }
370
+ if (!config.entity[baseEntity].baseUrls || !Array.isArray(config.entity[baseEntity].baseUrls) || config.entity[baseEntity].baseUrls.length < 1) {
371
+ const err = new Error(`missing configuration entity.${baseEntity}.baseUrls`)
372
+ throw err
263
373
  }
264
-
265
374
  urlObj = new URL(config.entity[baseEntity].baseUrls[0])
266
375
  const param = {
267
376
  baseUrl: config.entity[baseEntity].baseUrls[0],
@@ -269,17 +378,46 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
269
378
  json: true, // json-object response instead of string
270
379
  headers: {
271
380
  'Content-Type': 'application/json',
272
- // Auth PassThrough or configuration, using ctx "AS-IS" header for PassThrough. For more advanced logic use getCtxAuth(ctx) - see examples in other plugins
273
- Authorization: ctx?.request?.header?.authorization ? ctx.request.header.authorization : 'Basic ' + Buffer.from(`${config.entity[baseEntity].username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)}`).toString('base64')
381
+ Accept: 'application/json'
274
382
  },
275
383
  host: urlObj.hostname,
276
384
  port: urlObj.port, // null if https and 443 defined in url
277
- protocol: urlObj.protocol, // http: or https:
278
- rejectUnauthorized: false // accepts self-siged certificates
385
+ protocol: urlObj.protocol // http: or https:
279
386
  // 'method' and 'path' added at the end
280
387
  }
281
388
  }
282
389
 
390
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough using ctx header
391
+ param.options.headers.Authorization = ctx.request.header.authorization
392
+ } else {
393
+ switch (authType) {
394
+ case 'basicAuth':
395
+ if (!config.entity[baseEntity].basicAuth.username || !config.entity[baseEntity].basicAuth.password) {
396
+ const err = new Error(`missing configuration entity.${baseEntity}.basicAuth.username/password`)
397
+ throw err
398
+ }
399
+ param.options.headers.Authorization = 'Basic ' + Buffer.from(`${config.entity[baseEntity].basicAuth.username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.basicAuth.password`, configFile)}`).toString('base64')
400
+ break
401
+ case 'oauth':
402
+ if (!config.entity[baseEntity].oauth.clientId || !config.entity[baseEntity].oauth.clientSecret) {
403
+ const err = new Error(`missing configuration entity.${baseEntity}.oauth.clientId/clientSecret`)
404
+ throw err
405
+ }
406
+ param.accessToken = await getAccessToken(baseEntity, ctx)
407
+ param.options.headers.Authorization = `Bearer ${param.accessToken.access_token}`
408
+ break
409
+ case 'bearerAuth':
410
+ if (!config.entity[baseEntity].bearerAuth.token) {
411
+ const err = new Error(`missing configuration entity.${baseEntity}.bearerAuth.token`)
412
+ throw err
413
+ }
414
+ param.options.headers.Authorization = 'Bearer ' + Buffer.from(`${scimgateway.getPassword(`endpoint.entity.${baseEntity}.bearerAuth.token`, configFile)}`).toString('base64')
415
+ break
416
+ default:
417
+ // no auth
418
+ }
419
+ }
420
+
283
421
  // proxy
284
422
  if (config.entity[baseEntity].proxy && config.entity[baseEntity].proxy.host) {
285
423
  const agent = new HttpsProxyAgent(config.entity[baseEntity].proxy.host)
@@ -289,9 +427,17 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
289
427
  }
290
428
  }
291
429
 
430
+ // config options
431
+ if (config.entity[baseEntity].options) param.options = scimgateway.extendObj(param.options, config.entity[baseEntity].options)
432
+
292
433
  if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
293
434
  if (!_serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = {}
294
435
  _serviceClient[baseEntity][clientIdentifier] = param // serviceClient created
436
+
437
+ // OData support - note, not using [clientIdentifier]
438
+ _serviceClient[baseEntity].nextLink = {}
439
+ _serviceClient[baseEntity].nextLink.users = null // Azure users pagination
440
+ _serviceClient[baseEntity].nextLink.groups = null // Azure groups pagination
295
441
  }
296
442
 
297
443
  const cli = scimgateway.copyObj(_serviceClient[baseEntity][clientIdentifier]) // client ready
@@ -358,9 +504,11 @@ const updateServiceClient = (baseEntity, clientIdentifier, obj) => {
358
504
  // doRequest - execute REST service
359
505
  //
360
506
  const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) => {
507
+ let retryAfter = 0
361
508
  try {
362
509
  const cli = await getServiceClient(baseEntity, method, path, opt, ctx)
363
510
  const options = cli.options
511
+
364
512
  const result = await new Promise((resolve, reject) => {
365
513
  let dataString = ''
366
514
  if (body) {
@@ -391,7 +539,14 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
391
539
  try {
392
540
  if (responseString) response.body = JSON.parse(responseString)
393
541
  } catch (err) { response.body = responseString }
394
- if (statusCode < 200 || statusCode > 299) reject(new Error(JSON.stringify(response)))
542
+ if (statusCode < 200 || statusCode > 299) {
543
+ if (statusCode === 429) { // throttle
544
+ const v = res.headers['retry-after']
545
+ if (!isNaN(v)) retryAfter = parseInt(v, 10) + 1
546
+ else retryAfter = 10
547
+ }
548
+ reject(new Error(JSON.stringify(response)))
549
+ }
395
550
  resolve(response)
396
551
  })
397
552
  }) // req
@@ -410,17 +565,28 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
410
565
  req.end()
411
566
  }) // Promise
412
567
 
413
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${options.protocol}//${options.host}${(options.port ? `:${options.port}` : '')}${path} Body = ${JSON.stringify(body)} Response = ${JSON.stringify(result)}`)
568
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${options.protocol}//${options.host}${(options.port ? `:${options.port}` : '')}${options.path} Body = ${JSON.stringify(body)} Response = ${JSON.stringify(result)}`)
414
569
  return result
415
570
  } catch (err) { // includes failover/retry logic based on config baseUrls array
416
- scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
417
571
  let statusCode
418
572
  try { statusCode = JSON.parse(err.message).statusCode } catch (e) {}
573
+ if (statusCode === 404) { // not logged as error, let caller decide e.g. getUser-manager
574
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
575
+ } else scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
419
576
  const clientIdentifier = getClientIdentifier(ctx)
577
+ if (err.message.includes('ratelimit')) { // have seen throttling not follow standard 429/retry-after, but instead using 500 and error message only
578
+ if (!retryAfter) retryAfter = 60
579
+ }
420
580
  if (!retryCount) retryCount = 0
421
581
  let urlObj
422
582
  try { urlObj = new URL(path) } catch (err) {}
423
- if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND')) {
583
+ if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT' || retryAfter)) {
584
+ if (retryAfter) {
585
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${path} throttle/ratelimit error - awaiting ${retryAfter} seconds before automatic retry`)
586
+ await new Promise((resolve, reject) => setTimeout(function () {
587
+ resolve()
588
+ }, retryAfter * 1000))
589
+ }
424
590
  if (retryCount < config.entity[baseEntity].baseUrls.length) {
425
591
  retryCount++
426
592
  updateServiceClient(baseEntity, clientIdentifier, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
@@ -435,11 +601,15 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
435
601
  }
436
602
  } else {
437
603
  if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
438
- throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
604
+ throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
439
605
  }
440
606
  }
441
607
  } // doRequest
442
608
 
609
+ //
610
+ // end - REST endpoint template
611
+ //
612
+
443
613
  //
444
614
  // Cleanup on exit
445
615
  //
@@ -74,7 +74,6 @@ const URL = require('url').URL
74
74
  const querystring = require('querystring')
75
75
 
76
76
  // mandatory plugin initialization - start
77
- const path = require('path')
78
77
  let ScimGateway = null
79
78
  try {
80
79
  ScimGateway = require('scimgateway')
@@ -82,9 +81,9 @@ try {
82
81
  ScimGateway = require('./scimgateway')
83
82
  }
84
83
  const scimgateway = new ScimGateway()
85
- const pluginName = path.basename(__filename, '.js')
86
- const configDir = path.join(__dirname, '..', 'config')
87
- const configFile = path.join(`${configDir}`, `${pluginName}.json`)
84
+ const pluginName = scimgateway.pluginName
85
+ // const configDir = scimgateway.configDir
86
+ const configFile = scimgateway.configFile
88
87
  let config = require(configFile).endpoint
89
88
  config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file
90
89
  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
@@ -1301,6 +1300,7 @@ const updateServiceClient = (baseEntity, clientIdentifier, obj) => {
1301
1300
  // doRequest - execute REST service
1302
1301
  //
1303
1302
  const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) => {
1303
+ let retryAfter = 0
1304
1304
  try {
1305
1305
  const cli = await getServiceClient(baseEntity, method, path, opt, ctx)
1306
1306
  const options = cli.options
@@ -1335,7 +1335,14 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
1335
1335
  try {
1336
1336
  if (responseString) response.body = JSON.parse(responseString)
1337
1337
  } catch (err) { response.body = responseString }
1338
- if (statusCode < 200 || statusCode > 299) reject(new Error(JSON.stringify(response)))
1338
+ if (statusCode < 200 || statusCode > 299) {
1339
+ if (statusCode === 429) { // throttle
1340
+ const v = res.headers['retry-after']
1341
+ if (!isNaN(v)) retryAfter = parseInt(v, 10) + 1
1342
+ else retryAfter = 10
1343
+ }
1344
+ reject(new Error(JSON.stringify(response)))
1345
+ }
1339
1346
  resolve(response)
1340
1347
  })
1341
1348
  }) // req
@@ -1363,10 +1370,19 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
1363
1370
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
1364
1371
  } else scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
1365
1372
  const clientIdentifier = getClientIdentifier(ctx)
1373
+ if (err.message.includes('ratelimit')) { // have seen throttling not follow standard 429/retry-after, but instead using 500 and error message only
1374
+ if (!retryAfter) retryAfter = 60
1375
+ }
1366
1376
  if (!retryCount) retryCount = 0
1367
1377
  let urlObj
1368
1378
  try { urlObj = new URL(path) } catch (err) {}
1369
- if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')) {
1379
+ if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT' || retryAfter)) {
1380
+ if (retryAfter) {
1381
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${path} throttle/ratelimit error - awaiting ${retryAfter} seconds before automatic retry`)
1382
+ await new Promise((resolve, reject) => setTimeout(function () {
1383
+ resolve()
1384
+ }, retryAfter * 1000))
1385
+ }
1370
1386
  if (retryCount < config.entity[baseEntity].baseUrls.length) {
1371
1387
  retryCount++
1372
1388
  updateServiceClient(baseEntity, clientIdentifier, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
@@ -72,7 +72,6 @@
72
72
  const ldap = require('ldapjs')
73
73
 
74
74
  // mandatory plugin initialization - start
75
- const path = require('path')
76
75
  let ScimGateway = null
77
76
  try {
78
77
  ScimGateway = require('scimgateway')
@@ -80,9 +79,9 @@ try {
80
79
  ScimGateway = require('./scimgateway')
81
80
  }
82
81
  const scimgateway = new ScimGateway()
83
- const pluginName = path.basename(__filename, '.js')
84
- const configDir = path.join(__dirname, '..', 'config')
85
- const configFile = path.join(`${configDir}`, `${pluginName}.json`)
82
+ const pluginName = scimgateway.pluginName
83
+ // const configDir = scimgateway.configDir
84
+ const configFile = scimgateway.configFile
86
85
  let config = require(configFile).endpoint
87
86
  config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file
88
87
  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
@@ -1055,7 +1054,7 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1055
1054
  options.paged = { pageSize: 200, pagePause: false } // parse entire directory calling 'page' method for each page
1056
1055
  result = await new Promise((resolve, reject) => {
1057
1056
  const results = []
1058
- client.search(base, options, (err, search) => {
1057
+ client.search(base, scimgateway.copyObj(options), (err, search) => {
1059
1058
  if (err) {
1060
1059
  return reject(err)
1061
1060
  }
@@ -27,9 +27,9 @@
27
27
  'use strict'
28
28
 
29
29
  const Loki = require('lokijs')
30
+ const path = require('path')
30
31
 
31
32
  // mandatory plugin initialization - start
32
- const path = require('path')
33
33
  let ScimGateway = null
34
34
  try {
35
35
  ScimGateway = require('scimgateway')
@@ -37,10 +37,9 @@ try {
37
37
  ScimGateway = require('./scimgateway')
38
38
  }
39
39
  const scimgateway = new ScimGateway()
40
- const pluginName = path.basename(__filename, '.js')
41
- const configDir = path.join(__dirname, '..', 'config')
42
- const configFile = path.join(`${configDir}`, `${pluginName}.json`)
43
- const validScimAttr = [] // empty array - all attrbutes are supported by endpoint
40
+ const pluginName = scimgateway.pluginName
41
+ const configDir = scimgateway.configDir
42
+ const configFile = scimgateway.configFile
44
43
  let config = require(configFile).endpoint
45
44
  config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file
46
45
  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
@@ -105,7 +104,6 @@ for (const baseEntity in config.entity) {
105
104
  config.entity[baseEntity].groups = groups
106
105
  }
107
106
 
108
- // if (db.options.autoload === false) loadHandler(baseEntity)
109
107
  if (!isPersisence) loadHandler()
110
108
  }
111
109
 
@@ -222,10 +220,6 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
222
220
 
223
221
  if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
224
222
  const users = config.entity[baseEntity].users
225
- const notValid = scimgateway.notValidAttributes(userObj, validScimAttr) // We should check for unsupported endpoint attributes
226
- if (notValid) {
227
- throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
228
- }
229
223
 
230
224
  if (userObj.password) delete userObj.password // exclude password db not ecrypted
231
225
  for (const key in userObj) {
@@ -281,10 +275,6 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
281
275
 
282
276
  if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
283
277
  const users = config.entity[baseEntity].users
284
- const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr) // We should check for unsupported endpoint attributes
285
- if (notValid) {
286
- throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
287
- }
288
278
  if (attrObj.password) delete attrObj.password // exclude password db not ecrypted
289
279
 
290
280
  const res = users.find({ id: id })
@@ -27,7 +27,6 @@
27
27
  const MongoClient = require('mongodb').MongoClient
28
28
 
29
29
  // mandatory plugin initialization - start
30
- const path = require('path')
31
30
  let ScimGateway = null
32
31
  try {
33
32
  ScimGateway = require('./scimgateway')
@@ -35,10 +34,9 @@ try {
35
34
  ScimGateway = require('scimgateway')
36
35
  }
37
36
  const scimgateway = new ScimGateway()
38
- const pluginName = path.basename(__filename, '.js')
39
- const configDir = path.join(__dirname, '..', 'config')
40
- const configFile = path.join(`${configDir}`, `${pluginName}.json`)
41
- const validScimAttr = [] // empty array - all attrbutes are supported by endpoint
37
+ const pluginName = scimgateway.pluginName
38
+ // const configDir = scimgateway.configDir
39
+ const configFile = scimgateway.configFile
42
40
  let config = require(configFile).endpoint
43
41
  config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file
44
42
  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
@@ -280,11 +278,6 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
280
278
 
281
279
  const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
282
280
 
283
- const notValid = scimgateway.notValidAttributes(userObj, validScimAttr) // We should check for unsupported endpoint attributes
284
- if (notValid) {
285
- throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
286
- }
287
-
288
281
  if (userObj.password) delete userObj.password // exclude password db not ecrypted
289
282
  for (const key in userObj) {
290
283
  if (!Array.isArray(userObj[key]) && scimgateway.isMultiValueTypes(key)) { // true if attribute is "type converted object" => convert to standard array
@@ -363,14 +356,8 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
363
356
 
364
357
  const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
365
358
 
366
- const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr) // We should check for unsupported endpoint attributes
367
- if (notValid) {
368
- throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
369
- }
370
359
  if (attrObj.password) delete attrObj.password // exclude password db not ecrypted
371
-
372
360
  let res
373
-
374
361
  try {
375
362
  const users = config.entity[baseEntity][clientIdentifier].collection.users
376
363
  res = await users.find({ id }, { projection: { _id: 0 } }).toArray()
@@ -38,7 +38,6 @@ const Connection = require('tedious').Connection
38
38
  const Request = require('tedious').Request
39
39
 
40
40
  // mandatory plugin initialization - start
41
- const path = require('path')
42
41
  let ScimGateway = null
43
42
  try {
44
43
  ScimGateway = require('scimgateway')
@@ -46,21 +45,9 @@ try {
46
45
  ScimGateway = require('./scimgateway')
47
46
  }
48
47
  const scimgateway = new ScimGateway()
49
- const pluginName = path.basename(__filename, '.js')
50
- const configDir = path.join(__dirname, '..', 'config')
51
- const configFile = path.join(`${configDir}`, `${pluginName}.json`)
52
- const validScimAttr = [ // array containing scim attributes supported by our plugin code. Empty array - all attrbutes are supported by endpoint
53
- 'userName', // userName is mandatory
54
- 'active', // active is mandatory
55
- 'password',
56
- 'name.givenName',
57
- 'name.middleName',
58
- 'name.familyName',
59
- // "emails", // accepts all multivalues for this key
60
- 'emails.work', // accepts multivalues if type value equal work (lowercase)
61
- // "phoneNumbers",
62
- 'phoneNumbers.work'
63
- ]
48
+ const pluginName = scimgateway.pluginName
49
+ // const configDir = scimgateway.configDir
50
+ const configFile = scimgateway.configFile
64
51
  let config = require(configFile).endpoint
65
52
  config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file
66
53
  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
@@ -181,12 +168,6 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
181
168
 
182
169
  try {
183
170
  return await new Promise((resolve, reject) => {
184
- const notValid = scimgateway.notValidAttributes(userObj, validScimAttr)
185
- if (notValid) {
186
- const err = Error(`unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
187
- return reject(err)
188
- }
189
-
190
171
  if (!userObj.name) userObj.name = {}
191
172
  if (!userObj.emails) userObj.emails = { work: {} }
192
173
  if (!userObj.phoneNumbers) userObj.phoneNumbers = { work: {} }
@@ -292,12 +273,6 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
292
273
 
293
274
  try {
294
275
  return await new Promise((resolve, reject) => {
295
- const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr)
296
- if (notValid) {
297
- const err = new Error(`unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
298
- return reject(err)
299
- }
300
-
301
276
  if (!attrObj.name) attrObj.name = {}
302
277
  if (!attrObj.emails) attrObj.emails = { work: {} }
303
278
  if (!attrObj.phoneNumbers) attrObj.phoneNumbers = { work: {} }
@@ -22,7 +22,6 @@
22
22
  const hdb = require('hdb')
23
23
 
24
24
  // mandatory plugin initialization - start
25
- const path = require('path')
26
25
  let ScimGateway = null
27
26
  try {
28
27
  ScimGateway = require('scimgateway')
@@ -30,13 +29,9 @@ try {
30
29
  ScimGateway = require('./scimgateway')
31
30
  }
32
31
  const scimgateway = new ScimGateway()
33
- const pluginName = path.basename(__filename, '.js')
34
- const configDir = path.join(__dirname, '..', 'config')
35
- const configFile = path.join(`${configDir}`, `${pluginName}.json`)
36
- const validScimAttr = [ // array containing scim attributes supported by our plugin code. Empty array - all attrbutes are supported by endpoint
37
- 'userName', // userName is mandatory
38
- 'active' // active is mandatory
39
- ]
32
+ const pluginName = scimgateway.pluginName
33
+ // const configDir = scimgateway.configDir
34
+ const configFile = scimgateway.configFile
40
35
  let config = require(configFile).endpoint
41
36
  config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file
42
37
  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
@@ -143,13 +138,6 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
143
138
 
144
139
  try {
145
140
  return await new Promise((resolve, reject) => {
146
- const notValid = scimgateway.notValidAttributes(userObj, validScimAttr)
147
-
148
- if (notValid) {
149
- const err = new Error(`unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
150
- return reject(err)
151
- }
152
-
153
141
  hdbClient.connect(function (err) {
154
142
  if (err) {
155
143
  const newErr = new Error('createUser hdbcClient.connect: SAP Hana client connect error: ' + err.message)
@@ -221,12 +209,6 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
221
209
 
222
210
  try {
223
211
  return await new Promise((resolve, reject) => {
224
- const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr)
225
- if (notValid) {
226
- const err = new Error(`unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
227
- return reject(err)
228
- }
229
-
230
212
  let sqlAction = ''
231
213
  if (attrObj.active !== undefined) {
232
214
  if (sqlAction.length === 0) sqlAction = (attrObj.active === true) ? 'ACTIVATE' : 'DEACTIVATE'