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/README.md +41 -6
- package/config/plugin-api.json +28 -3
- package/config/plugin-entra-id.json +27 -1
- package/config/plugin-ldap.json +24 -0
- package/config/plugin-loki.json +24 -0
- package/config/plugin-mongodb.json +24 -0
- package/config/plugin-mssql.json +24 -0
- package/config/plugin-saphana.json +24 -0
- package/config/plugin-scim.json +28 -0
- package/config/plugin-soap.json +24 -0
- package/index.js +2 -2
- package/lib/plugin-api.js +187 -17
- package/lib/plugin-entra-id.js +22 -6
- package/lib/plugin-ldap.js +4 -5
- package/lib/plugin-loki.js +4 -14
- package/lib/plugin-mongodb.js +3 -16
- package/lib/plugin-mssql.js +3 -28
- package/lib/plugin-saphana.js +3 -21
- package/lib/plugin-scim.js +22 -31
- package/lib/plugin-soap.js +6 -30
- package/lib/scim-stream.js +13 -0
- package/lib/scimgateway.js +96 -20
- package/package.json +6 -6
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 =
|
|
45
|
-
const configDir =
|
|
46
|
-
const configFile =
|
|
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) => {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
//
|
package/lib/plugin-entra-id.js
CHANGED
|
@@ -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 =
|
|
86
|
-
const configDir =
|
|
87
|
-
const configFile =
|
|
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)
|
|
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] })
|
package/lib/plugin-ldap.js
CHANGED
|
@@ -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 =
|
|
84
|
-
const configDir =
|
|
85
|
-
const configFile =
|
|
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
|
}
|
package/lib/plugin-loki.js
CHANGED
|
@@ -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 =
|
|
41
|
-
const configDir =
|
|
42
|
-
const configFile =
|
|
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 })
|
package/lib/plugin-mongodb.js
CHANGED
|
@@ -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 =
|
|
39
|
-
const configDir =
|
|
40
|
-
const configFile =
|
|
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()
|
package/lib/plugin-mssql.js
CHANGED
|
@@ -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 =
|
|
50
|
-
const configDir =
|
|
51
|
-
const configFile =
|
|
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: {} }
|
package/lib/plugin-saphana.js
CHANGED
|
@@ -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 =
|
|
34
|
-
const configDir =
|
|
35
|
-
const configFile =
|
|
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'
|