scimgateway 4.2.17 → 4.3.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 +66 -50
- package/config/{plugin-azure-ad.json → plugin-entra-id.json} +10 -7
- package/config/plugin-loki.json +7 -3
- package/config/plugin-scim.json +9 -5
- package/index.js +1 -1
- package/lib/{plugin-azure-ad.js → plugin-entra-id.js} +243 -171
- package/lib/plugin-ldap.js +16 -7
- package/lib/plugin-loki.js +77 -38
- package/lib/plugin-scim.js +166 -12
- package/lib/postinstall.js +2 -2
- package/lib/scimgateway.js +75 -85
- package/lib/utils.js +19 -5
- package/package.json +9 -10
package/lib/plugin-loki.js
CHANGED
|
@@ -48,49 +48,66 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
|
|
|
48
48
|
|
|
49
49
|
// let endpointPasswordExample = scimgateway.getPassword('endpoint.password', configFile); // example how to encrypt configfile having "endpoint.password"
|
|
50
50
|
|
|
51
|
-
let users
|
|
52
|
-
let groups
|
|
53
51
|
const validFilterOperators = ['eq', 'ne', 'aeq', 'dteq', 'gt', 'gte', 'lt', 'lte', 'between', 'jgt', 'jgte', 'jlt', 'jlte', 'jbetween', 'regex', 'in', 'nin', 'keyin', 'nkeyin', 'definedin', 'undefinedin', 'contains', 'containsAny', 'type', 'finite', 'size', 'len', 'exists']
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
autosave: config.persistence !== false,
|
|
62
|
-
autosaveInterval: 10000, // 10 seconds
|
|
63
|
-
adapter: (config.persistence !== false) ? new Loki.LokiFsAdapter() : new Loki.LokiMemoryAdapter()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
function loadHandler () {
|
|
67
|
-
users = db.getCollection('users')
|
|
68
|
-
if (users === null) { // if database do not exist it will be empty so intitialize here
|
|
69
|
-
users = db.addCollection('users', {
|
|
70
|
-
unique: ['id', 'userName']
|
|
71
|
-
})
|
|
53
|
+
const dbNames = []
|
|
54
|
+
for (const baseEntity in config.entity) {
|
|
55
|
+
let dbname = (config.entity[baseEntity].dbname ? config.entity[baseEntity].dbname : 'loki.db')
|
|
56
|
+
if (dbNames.includes(dbname)) {
|
|
57
|
+
scimgateway.logger.error(`${pluginName}[${baseEntity}] initialization error: database '${dbname}' is already used by another baseEntity configuration`)
|
|
58
|
+
continue
|
|
72
59
|
}
|
|
60
|
+
dbNames.push(dbname)
|
|
61
|
+
dbname = path.join(`${configDir}`, `${dbname}`)
|
|
62
|
+
const isPersisence = config.entity[baseEntity].persistence !== false
|
|
63
|
+
|
|
64
|
+
const db = new Loki(dbname, {
|
|
65
|
+
env: baseEntity === 'undefined' ? 'N/A' : baseEntity, // avoid default NODEJS
|
|
66
|
+
autoload: isPersisence,
|
|
67
|
+
autoloadCallback: isPersisence ? loadHandler : null,
|
|
68
|
+
autosave: isPersisence,
|
|
69
|
+
autosaveInterval: 10000, // 10 seconds
|
|
70
|
+
adapter: isPersisence ? new Loki.LokiFsAdapter() : new Loki.LokiMemoryAdapter()
|
|
71
|
+
})
|
|
72
|
+
config.entity[baseEntity].db = db
|
|
73
|
+
|
|
74
|
+
function loadHandler () {
|
|
75
|
+
let users = db.getCollection('users')
|
|
76
|
+
if (users === null) { // if database do not exist it will be empty so intitialize here
|
|
77
|
+
users = db.addCollection('users', {
|
|
78
|
+
unique: ['id', 'userName']
|
|
79
|
+
})
|
|
80
|
+
}
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
let groups = db.getCollection('groups')
|
|
83
|
+
if (groups === null) {
|
|
84
|
+
groups = db.addCollection('groups', {
|
|
85
|
+
unique: ['displayName']
|
|
86
|
+
})
|
|
87
|
+
}
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
if (!isPersisence) { // load testusers
|
|
90
|
+
scimgateway.testmodeusers.forEach(record => {
|
|
91
|
+
const r = scimgateway.copyObj(record)
|
|
92
|
+
if (r.meta) delete r.meta
|
|
93
|
+
users.insert(r)
|
|
94
|
+
})
|
|
95
|
+
scimgateway.testmodegroups.forEach(record => {
|
|
96
|
+
const r = scimgateway.copyObj(record)
|
|
97
|
+
if (r.meta) delete r.meta
|
|
98
|
+
groups.insert(r)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let baseEntity = db.ENV
|
|
103
|
+
if (baseEntity === 'N/A') baseEntity = undefined
|
|
104
|
+
config.entity[baseEntity].users = users
|
|
105
|
+
config.entity[baseEntity].groups = groups
|
|
90
106
|
}
|
|
91
|
-
}
|
|
92
107
|
|
|
93
|
-
if (db.options.autoload === false) loadHandler()
|
|
108
|
+
// if (db.options.autoload === false) loadHandler(baseEntity)
|
|
109
|
+
if (!isPersisence) loadHandler()
|
|
110
|
+
}
|
|
94
111
|
|
|
95
112
|
// =================================================
|
|
96
113
|
// getUsers
|
|
@@ -111,6 +128,9 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
111
128
|
const action = 'getUsers'
|
|
112
129
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
|
|
113
130
|
|
|
131
|
+
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
|
|
132
|
+
const users = config.entity[baseEntity].users
|
|
133
|
+
|
|
114
134
|
if (getObj.operator) { // convert to plugin supported syntax
|
|
115
135
|
switch (getObj.operator) {
|
|
116
136
|
case 'co':
|
|
@@ -200,6 +220,8 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
|
|
|
200
220
|
const action = 'createUser'
|
|
201
221
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" userObj=${JSON.stringify(userObj)}`)
|
|
202
222
|
|
|
223
|
+
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
|
|
224
|
+
const users = config.entity[baseEntity].users
|
|
203
225
|
const notValid = scimgateway.notValidAttributes(userObj, validScimAttr) // We should check for unsupported endpoint attributes
|
|
204
226
|
if (notValid) {
|
|
205
227
|
throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
|
|
@@ -241,6 +263,8 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
|
|
|
241
263
|
const action = 'deleteUser'
|
|
242
264
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
|
|
243
265
|
|
|
266
|
+
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
|
|
267
|
+
const users = config.entity[baseEntity].users
|
|
244
268
|
const res = users.find({ id: id })
|
|
245
269
|
if (res.length !== 1) throw new Error(`${action} error: failed for user id=${id}`)
|
|
246
270
|
const userObj = res[0]
|
|
@@ -255,6 +279,8 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
255
279
|
const action = 'modifyUser'
|
|
256
280
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
257
281
|
|
|
282
|
+
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
|
|
283
|
+
const users = config.entity[baseEntity].users
|
|
258
284
|
const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr) // We should check for unsupported endpoint attributes
|
|
259
285
|
if (notValid) {
|
|
260
286
|
throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
|
|
@@ -374,6 +400,9 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
374
400
|
const action = 'getGroups'
|
|
375
401
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
|
|
376
402
|
|
|
403
|
+
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
|
|
404
|
+
const groups = config.entity[baseEntity].groups
|
|
405
|
+
|
|
377
406
|
if (getObj.operator) { // convert to plugin supported syntax
|
|
378
407
|
switch (getObj.operator) {
|
|
379
408
|
case 'co':
|
|
@@ -463,6 +492,8 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
|
|
|
463
492
|
const action = 'createGroup'
|
|
464
493
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
|
|
465
494
|
|
|
495
|
+
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
|
|
496
|
+
const groups = config.entity[baseEntity].groups
|
|
466
497
|
if (groupObj.externalId) groupObj.id = groupObj.externalId // for loki-plugin (scim endpoint) id is mandatory and set to displayName
|
|
467
498
|
else groupObj.id = groupObj.displayName
|
|
468
499
|
|
|
@@ -485,6 +516,8 @@ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
|
|
|
485
516
|
const action = 'deleteGroup'
|
|
486
517
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
|
|
487
518
|
|
|
519
|
+
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
|
|
520
|
+
const groups = config.entity[baseEntity].groups
|
|
488
521
|
const res = groups.find({ id: id })
|
|
489
522
|
if (res.length !== 1) throw new Error(`${action} error: failed for id=${id}`)
|
|
490
523
|
const groupObj = res[0]
|
|
@@ -499,6 +532,8 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
499
532
|
const action = 'modifyGroup'
|
|
500
533
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
501
534
|
|
|
535
|
+
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
|
|
536
|
+
const groups = config.entity[baseEntity].groups
|
|
502
537
|
const res = groups.find({ id: id })
|
|
503
538
|
if (res.length === 0) throw new Error(`${action} error: group id=${id} - group does not exist`)
|
|
504
539
|
if (res.length > 1) throw new Error(`${action} error: group id=${id} - group is not unique, more than one have been found`)
|
|
@@ -569,8 +604,12 @@ const stripLoki = (obj) => { // remove loki meta data and insert scim
|
|
|
569
604
|
// Cleanup on exit
|
|
570
605
|
//
|
|
571
606
|
process.on('SIGTERM', () => { // kill
|
|
572
|
-
|
|
607
|
+
for (const baseEntity in config.entity) {
|
|
608
|
+
if (config.entity[baseEntity].db) config.entity[baseEntity].db.close()
|
|
609
|
+
}
|
|
573
610
|
})
|
|
574
611
|
process.on('SIGINT', () => { // Ctrl+C
|
|
575
|
-
|
|
612
|
+
for (const baseEntity in config.entity) {
|
|
613
|
+
if (config.entity[baseEntity].db) config.entity[baseEntity].db.close()
|
|
614
|
+
}
|
|
576
615
|
})
|
package/lib/plugin-scim.js
CHANGED
|
@@ -65,6 +65,7 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
|
|
|
65
65
|
// mandatory plugin initialization - end
|
|
66
66
|
|
|
67
67
|
const _serviceClient = {}
|
|
68
|
+
const lock = new scimgateway.Lock()
|
|
68
69
|
|
|
69
70
|
// =================================================
|
|
70
71
|
// getUsers
|
|
@@ -552,6 +553,10 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
552
553
|
// helpers
|
|
553
554
|
// =================================================
|
|
554
555
|
|
|
556
|
+
//
|
|
557
|
+
// start - REST endpoint template
|
|
558
|
+
//
|
|
559
|
+
|
|
555
560
|
const getClientIdentifier = (ctx) => {
|
|
556
561
|
if (!ctx?.request?.header?.authorization) return undefined
|
|
557
562
|
const [user, secret] = getCtxAuth(ctx)
|
|
@@ -561,7 +566,7 @@ const getClientIdentifier = (ctx) => {
|
|
|
561
566
|
//
|
|
562
567
|
// getCtxAuth returns username/secret from ctx header when using Auth PassThrough
|
|
563
568
|
//
|
|
564
|
-
const getCtxAuth = (ctx) => {
|
|
569
|
+
const getCtxAuth = (ctx) => {
|
|
565
570
|
if (!ctx?.request?.header?.authorization) return []
|
|
566
571
|
const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
|
|
567
572
|
let username, password
|
|
@@ -570,6 +575,85 @@ const getCtxAuth = (ctx) => { // eslint-disable-line
|
|
|
570
575
|
else return [undefined, authToken] // bearer auth
|
|
571
576
|
}
|
|
572
577
|
|
|
578
|
+
//
|
|
579
|
+
// getAccessToken - returns oauth accesstoken
|
|
580
|
+
//
|
|
581
|
+
const getAccessToken = async (baseEntity, ctx) => {
|
|
582
|
+
await lock.acquire()
|
|
583
|
+
const clientIdentifier = getClientIdentifier(ctx)
|
|
584
|
+
const d = new Date() / 1000 // seconds (unix time)
|
|
585
|
+
if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken &&
|
|
586
|
+
(_serviceClient[baseEntity][clientIdentifier].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
|
|
587
|
+
lock.release()
|
|
588
|
+
return _serviceClient[baseEntity][clientIdentifier].accessToken
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const action = 'getAccessToken'
|
|
592
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Retrieving accesstoken`)
|
|
593
|
+
|
|
594
|
+
const method = 'POST'
|
|
595
|
+
const [, secret] = getCtxAuth(ctx) // if Auth PassTrough, secret from basic or bearer auth
|
|
596
|
+
let tokenUrl
|
|
597
|
+
let form
|
|
598
|
+
|
|
599
|
+
if (config.entity[baseEntity].oauth) {
|
|
600
|
+
let resource
|
|
601
|
+
try {
|
|
602
|
+
const urlObj = new URL(config.entity[baseEntity].baseUrls[0])
|
|
603
|
+
resource = urlObj.origin
|
|
604
|
+
} catch (err) {
|
|
605
|
+
resource = null
|
|
606
|
+
}
|
|
607
|
+
if (config.entity[baseEntity].oauth.tenantIdGUID) { // Azure
|
|
608
|
+
tokenUrl = `https://login.microsoftonline.com/${config.entity[baseEntity].oauth.tenantIdGUID}/oauth2/token`
|
|
609
|
+
} else {
|
|
610
|
+
tokenUrl = `https://login.microsoftonline.com/${config.entity[baseEntity].oauth.tokenUrl}`
|
|
611
|
+
}
|
|
612
|
+
form = {
|
|
613
|
+
grant_type: 'client_credentials',
|
|
614
|
+
client_id: config.entity[baseEntity].oauth.clientId,
|
|
615
|
+
client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.oauth.clientSecret`, configFile), // using config if no Auth PassThrough
|
|
616
|
+
resource: resource // "https://graph.microsoft.com"
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
const err = new Error(`[${action}] missing supported endpoint authentication configuration`)
|
|
620
|
+
lock.release()
|
|
621
|
+
throw (err)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const options = {
|
|
625
|
+
headers: {
|
|
626
|
+
'Content-Type': 'application/x-www-form-urlencoded' // body must be query string formatted (no JSON)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const response = await doRequest(baseEntity, method, tokenUrl, form, ctx, options)
|
|
632
|
+
if (!response.body) {
|
|
633
|
+
const err = new Error(`[${action}] No data retrieved from: ${method} ${tokenUrl}`)
|
|
634
|
+
throw (err)
|
|
635
|
+
}
|
|
636
|
+
const jbody = response.body
|
|
637
|
+
if (jbody.error) {
|
|
638
|
+
const err = new Error(`[${action}] Error message: ${jbody.error_description}`)
|
|
639
|
+
throw (err)
|
|
640
|
+
} else if (!jbody.access_token || !jbody.expires_in) {
|
|
641
|
+
const err = new Error(`[${action}] Error message: Retrieved invalid token response`)
|
|
642
|
+
throw (err)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const d = new Date() / 1000 // seconds (unix time)
|
|
646
|
+
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)
|
|
647
|
+
scimgateway.logger.silly(`${pluginName}[${baseEntity}] ${action}: AccessToken = ${jbody.access_token}`)
|
|
648
|
+
|
|
649
|
+
lock.release()
|
|
650
|
+
return jbody
|
|
651
|
+
} catch (err) {
|
|
652
|
+
lock.release()
|
|
653
|
+
throw (err)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
573
657
|
//
|
|
574
658
|
// getServiceClient - returns options needed for connection parameters
|
|
575
659
|
//
|
|
@@ -582,6 +666,11 @@ const getCtxAuth = (ctx) => { // eslint-disable-line
|
|
|
582
666
|
const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
|
|
583
667
|
const action = 'getServiceClient'
|
|
584
668
|
|
|
669
|
+
let authType
|
|
670
|
+
if (config.entity[baseEntity].basicAuth) authType = 'basicAuth'
|
|
671
|
+
else if (config.entity[baseEntity].oauth) authType = 'oauth'
|
|
672
|
+
else if (config.entity[baseEntity].bearerAuth) authType = 'bearerAuth'
|
|
673
|
+
|
|
585
674
|
let urlObj
|
|
586
675
|
if (!path) path = ''
|
|
587
676
|
try {
|
|
@@ -590,17 +679,38 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
|
|
|
590
679
|
//
|
|
591
680
|
// path (no url) - default approach and client will be cached based on config
|
|
592
681
|
//
|
|
682
|
+
|
|
593
683
|
const clientIdentifier = getClientIdentifier(ctx)
|
|
594
|
-
if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist
|
|
684
|
+
if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist - Azure plugin specific
|
|
595
685
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using existing client`)
|
|
686
|
+
if (_serviceClient[baseEntity][clientIdentifier].accessToken) {
|
|
687
|
+
// check if token refresh is needed when using oauth
|
|
688
|
+
const d = new Date() / 1000 // seconds (unix time)
|
|
689
|
+
if (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
|
|
690
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Accesstoken about to expire in ${_serviceClient[baseEntity][clientIdentifier].accessToken.validTo - d} seconds`)
|
|
691
|
+
try {
|
|
692
|
+
const accessToken = await getAccessToken(baseEntity, ctx)
|
|
693
|
+
_serviceClient[baseEntity][clientIdentifier].accessToken = accessToken
|
|
694
|
+
_serviceClient[baseEntity][clientIdentifier].options.headers.Authorization = ` Bearer ${accessToken.access_token}`
|
|
695
|
+
} catch (err) {
|
|
696
|
+
delete _serviceClient[baseEntity][clientIdentifier]
|
|
697
|
+
const newErr = err
|
|
698
|
+
throw newErr
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
596
702
|
} else {
|
|
597
703
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Client have to be created`)
|
|
598
704
|
let client = null
|
|
599
705
|
if (config.entity && config.entity[baseEntity]) client = config.entity[baseEntity]
|
|
600
706
|
if (!client) {
|
|
601
|
-
|
|
707
|
+
const err = new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`)
|
|
708
|
+
throw err
|
|
709
|
+
}
|
|
710
|
+
if (!config.entity[baseEntity].baseUrls || !Array.isArray(config.entity[baseEntity].baseUrls) || config.entity[baseEntity].baseUrls.length < 1) {
|
|
711
|
+
const err = new Error(`missing configuration entity.${baseEntity}.baseUrls`)
|
|
712
|
+
throw err
|
|
602
713
|
}
|
|
603
|
-
|
|
604
714
|
urlObj = new URL(config.entity[baseEntity].baseUrls[0])
|
|
605
715
|
const param = {
|
|
606
716
|
baseUrl: config.entity[baseEntity].baseUrls[0],
|
|
@@ -608,17 +718,46 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
|
|
|
608
718
|
json: true, // json-object response instead of string
|
|
609
719
|
headers: {
|
|
610
720
|
'Content-Type': 'application/json',
|
|
611
|
-
|
|
612
|
-
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')
|
|
721
|
+
Accept: 'application/json'
|
|
613
722
|
},
|
|
614
723
|
host: urlObj.hostname,
|
|
615
724
|
port: urlObj.port, // null if https and 443 defined in url
|
|
616
|
-
protocol: urlObj.protocol
|
|
617
|
-
rejectUnauthorized: false // accepts self-siged certificates
|
|
725
|
+
protocol: urlObj.protocol // http: or https:
|
|
618
726
|
// 'method' and 'path' added at the end
|
|
619
727
|
}
|
|
620
728
|
}
|
|
621
729
|
|
|
730
|
+
if (ctx?.request?.header?.authorization) { // Auth PassThrough using ctx header
|
|
731
|
+
param.options.headers.Authorization = ctx.request.header.authorization
|
|
732
|
+
} else {
|
|
733
|
+
switch (authType) {
|
|
734
|
+
case 'basicAuth':
|
|
735
|
+
if (!config.entity[baseEntity].basicAuth.username || !config.entity[baseEntity].basicAuth.password) {
|
|
736
|
+
const err = new Error(`missing configuration entity.${baseEntity}.basicAuth.username/password`)
|
|
737
|
+
throw err
|
|
738
|
+
}
|
|
739
|
+
param.options.headers.Authorization = 'Basic ' + Buffer.from(`${config.entity[baseEntity].basicAuth.username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.basicAuth.password`, configFile)}`).toString('base64')
|
|
740
|
+
break
|
|
741
|
+
case 'oauth':
|
|
742
|
+
if (!config.entity[baseEntity].oauth.clientId || !config.entity[baseEntity].oauth.clientSecret) {
|
|
743
|
+
const err = new Error(`missing configuration entity.${baseEntity}.oauth.clientId/clientSecret`)
|
|
744
|
+
throw err
|
|
745
|
+
}
|
|
746
|
+
param.accessToken = await getAccessToken(baseEntity, ctx)
|
|
747
|
+
param.options.headers.Authorization = `Bearer ${param.accessToken.access_token}`
|
|
748
|
+
break
|
|
749
|
+
case 'bearerAuth':
|
|
750
|
+
if (!config.entity[baseEntity].bearerAuth.token) {
|
|
751
|
+
const err = new Error(`missing configuration entity.${baseEntity}.bearerAuth.token`)
|
|
752
|
+
throw err
|
|
753
|
+
}
|
|
754
|
+
param.options.headers.Authorization = 'Bearer ' + Buffer.from(`${scimgateway.getPassword(`endpoint.entity.${baseEntity}.bearerAuth.token`, configFile)}`).toString('base64')
|
|
755
|
+
break
|
|
756
|
+
default:
|
|
757
|
+
// no auth
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
622
761
|
// proxy
|
|
623
762
|
if (config.entity[baseEntity].proxy && config.entity[baseEntity].proxy.host) {
|
|
624
763
|
const agent = new HttpsProxyAgent(config.entity[baseEntity].proxy.host)
|
|
@@ -628,9 +767,17 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
|
|
|
628
767
|
}
|
|
629
768
|
}
|
|
630
769
|
|
|
770
|
+
// config options
|
|
771
|
+
if (config.entity[baseEntity].options) param.options = scimgateway.extendObj(param.options, config.entity[baseEntity].options)
|
|
772
|
+
|
|
631
773
|
if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
|
|
632
774
|
if (!_serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = {}
|
|
633
775
|
_serviceClient[baseEntity][clientIdentifier] = param // serviceClient created
|
|
776
|
+
|
|
777
|
+
// OData support - note, not using [clientIdentifier]
|
|
778
|
+
_serviceClient[baseEntity].nextLink = {}
|
|
779
|
+
_serviceClient[baseEntity].nextLink.users = null // Azure users pagination
|
|
780
|
+
_serviceClient[baseEntity].nextLink.groups = null // Azure groups pagination
|
|
634
781
|
}
|
|
635
782
|
|
|
636
783
|
const cli = scimgateway.copyObj(_serviceClient[baseEntity][clientIdentifier]) // client ready
|
|
@@ -700,6 +847,7 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
|
|
|
700
847
|
try {
|
|
701
848
|
const cli = await getServiceClient(baseEntity, method, path, opt, ctx)
|
|
702
849
|
const options = cli.options
|
|
850
|
+
|
|
703
851
|
const result = await new Promise((resolve, reject) => {
|
|
704
852
|
let dataString = ''
|
|
705
853
|
if (body) {
|
|
@@ -749,17 +897,19 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
|
|
|
749
897
|
req.end()
|
|
750
898
|
}) // Promise
|
|
751
899
|
|
|
752
|
-
scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${options.protocol}//${options.host}${(options.port ? `:${options.port}` : '')}${path} Body = ${JSON.stringify(body)} Response = ${JSON.stringify(result)}`)
|
|
900
|
+
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)}`)
|
|
753
901
|
return result
|
|
754
902
|
} catch (err) { // includes failover/retry logic based on config baseUrls array
|
|
755
|
-
scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
|
|
756
903
|
let statusCode
|
|
757
904
|
try { statusCode = JSON.parse(err.message).statusCode } catch (e) {}
|
|
905
|
+
if (statusCode === 404) { // not logged as error, let caller decide e.g. getUser-manager
|
|
906
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
|
|
907
|
+
} else scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
|
|
758
908
|
const clientIdentifier = getClientIdentifier(ctx)
|
|
759
909
|
if (!retryCount) retryCount = 0
|
|
760
910
|
let urlObj
|
|
761
911
|
try { urlObj = new URL(path) } catch (err) {}
|
|
762
|
-
if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND')) {
|
|
912
|
+
if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')) {
|
|
763
913
|
if (retryCount < config.entity[baseEntity].baseUrls.length) {
|
|
764
914
|
retryCount++
|
|
765
915
|
updateServiceClient(baseEntity, clientIdentifier, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
|
|
@@ -774,11 +924,15 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
|
|
|
774
924
|
}
|
|
775
925
|
} else {
|
|
776
926
|
if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
|
|
777
|
-
throw err // CA IM retries
|
|
927
|
+
throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
|
|
778
928
|
}
|
|
779
929
|
}
|
|
780
930
|
} // doRequest
|
|
781
931
|
|
|
932
|
+
//
|
|
933
|
+
// end - REST endpoint template
|
|
934
|
+
//
|
|
935
|
+
|
|
782
936
|
//
|
|
783
937
|
// Cleanup on exit
|
|
784
938
|
//
|
package/lib/postinstall.js
CHANGED
|
@@ -33,7 +33,7 @@ if (!fsExistsSync('../../config/plugin-soap.json')) fs.writeFileSync('../../conf
|
|
|
33
33
|
if (!fsExistsSync('../../config/plugin-mssql.json')) fs.writeFileSync('../../config/plugin-mssql.json', fs.readFileSync('./config/plugin-mssql.json'))
|
|
34
34
|
if (!fsExistsSync('../../config/plugin-saphana.json')) fs.writeFileSync('../../config/plugin-saphana.json', fs.readFileSync('./config/plugin-saphana.json'))
|
|
35
35
|
if (!fsExistsSync('../../config/plugin-api.json')) fs.writeFileSync('../../config/plugin-api.json', fs.readFileSync('./config/plugin-api.json'))
|
|
36
|
-
if (!fsExistsSync('../../config/plugin-
|
|
36
|
+
if (!fsExistsSync('../../config/plugin-entra-id.json')) fs.writeFileSync('../../config/plugin-entra-id.json', fs.readFileSync('./config/plugin-entra-id.json')) // keep existing
|
|
37
37
|
if (!fsExistsSync('../../config/plugin-ldap.json')) fs.writeFileSync('../../config/plugin-ldap.json', fs.readFileSync('./config/plugin-ldap.json'))
|
|
38
38
|
if (!fsExistsSync('../../config/plugin-mongodb.json')) fs.writeFileSync('../../config/plugin-mongodb.json', fs.readFileSync('./config/plugin-mongodb.json'))
|
|
39
39
|
|
|
@@ -43,7 +43,7 @@ fs.writeFileSync('../../lib/plugin-soap.js', fs.readFileSync('./lib/plugin-soap.
|
|
|
43
43
|
fs.writeFileSync('../../lib/plugin-mssql.js', fs.readFileSync('./lib/plugin-mssql.js'))
|
|
44
44
|
fs.writeFileSync('../../lib/plugin-saphana.js', fs.readFileSync('./lib/plugin-saphana.js'))
|
|
45
45
|
fs.writeFileSync('../../lib/plugin-api.js', fs.readFileSync('./lib/plugin-api.js'))
|
|
46
|
-
fs.writeFileSync('../../lib/plugin-
|
|
46
|
+
fs.writeFileSync('../../lib/plugin-entra-id.js', fs.readFileSync('./lib/plugin-entra-id.js'))
|
|
47
47
|
fs.writeFileSync('../../lib/plugin-ldap.js', fs.readFileSync('./lib/plugin-ldap.js'))
|
|
48
48
|
fs.writeFileSync('../../lib/plugin-mongodb.js', fs.readFileSync('./lib/plugin-mongodb.js'))
|
|
49
49
|
|