scimgateway 4.2.2 → 4.2.5
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/.travis.yml +1 -1
- package/README.md +18 -0
- package/lib/plugin-api.js +55 -21
- package/lib/plugin-azure-ad.js +85 -45
- package/lib/plugin-forwardinc.js +35 -14
- package/lib/plugin-ldap.js +45 -29
- package/lib/plugin-loki.js +27 -18
- package/lib/plugin-mongodb.js +94 -37
- package/lib/plugin-mssql.js +58 -6
- package/lib/plugin-scim.js +55 -20
- package/lib/scimgateway.js +12 -26
- package/package.json +2 -2
package/lib/plugin-forwardinc.js
CHANGED
|
@@ -64,7 +64,10 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
|
|
|
64
64
|
|
|
65
65
|
const wsdlDir = path.join(`${configDir}`, 'wsdls')
|
|
66
66
|
const endpointUsername = config.username
|
|
67
|
-
|
|
67
|
+
let endpointPassword
|
|
68
|
+
if (!scimgateway.authPassThroughAllowed) { // not using Auth PassThrough
|
|
69
|
+
endpointPassword = scimgateway.getPassword('endpoint.password', configFile)
|
|
70
|
+
}
|
|
68
71
|
const _serviceClient = {}
|
|
69
72
|
|
|
70
73
|
// =================================================
|
|
@@ -119,7 +122,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
119
122
|
totalResults: null // not used - paging not supported
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
const serviceClient = await getServiceClient(baseEntity, soapAction)
|
|
125
|
+
const serviceClient = await getServiceClient(baseEntity, soapAction, ctx)
|
|
123
126
|
|
|
124
127
|
let result = await serviceClient[config[soapAction].method + 'Async'](soapRequest)
|
|
125
128
|
if (!Array.isArray(result) || result.length < 4) {
|
|
@@ -221,7 +224,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
|
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
226
|
|
|
224
|
-
const serviceClient = await getServiceClient(baseEntity, action)
|
|
227
|
+
const serviceClient = await getServiceClient(baseEntity, action, ctx)
|
|
225
228
|
|
|
226
229
|
let result = await serviceClient[config[action].method + 'Async'](soapRequest)
|
|
227
230
|
if (!Array.isArray(result) || result.length < 4) {
|
|
@@ -246,7 +249,7 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
|
|
|
246
249
|
const action = 'deleteUser'
|
|
247
250
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
|
|
248
251
|
try {
|
|
249
|
-
const serviceClient = await getServiceClient(baseEntity, action)
|
|
252
|
+
const serviceClient = await getServiceClient(baseEntity, action, ctx)
|
|
250
253
|
|
|
251
254
|
const soapRequest = { userID: id }
|
|
252
255
|
|
|
@@ -311,7 +314,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
311
314
|
} else userObj[key1] = attrObj[key1] // merge modified attr into userObj
|
|
312
315
|
}
|
|
313
316
|
|
|
314
|
-
const serviceClient = await getServiceClient(baseEntity, action)
|
|
317
|
+
const serviceClient = await getServiceClient(baseEntity, action, ctx)
|
|
315
318
|
|
|
316
319
|
const soapRequest = {
|
|
317
320
|
user: {
|
|
@@ -394,7 +397,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
394
397
|
}
|
|
395
398
|
|
|
396
399
|
try {
|
|
397
|
-
const serviceClient = await getServiceClient(baseEntity, soapAction)
|
|
400
|
+
const serviceClient = await getServiceClient(baseEntity, soapAction, ctx)
|
|
398
401
|
|
|
399
402
|
let result = await serviceClient[config[soapAction].method + 'Async'](soapRequest)
|
|
400
403
|
if (!Array.isArray(result) || result.length < 4) {
|
|
@@ -482,7 +485,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
482
485
|
}
|
|
483
486
|
|
|
484
487
|
try {
|
|
485
|
-
const serviceClient = await getServiceClient(baseEntity, action)
|
|
488
|
+
const serviceClient = await getServiceClient(baseEntity, action, ctx)
|
|
486
489
|
|
|
487
490
|
attrObj.members.forEach(async function (el) {
|
|
488
491
|
if (el.operation && el.operation === 'delete') { // delete member from group
|
|
@@ -526,7 +529,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
526
529
|
// helpers
|
|
527
530
|
// =================================================
|
|
528
531
|
|
|
529
|
-
const getServiceClient = async (baseEntity, action) => {
|
|
532
|
+
const getServiceClient = async (baseEntity, action, ctx) => {
|
|
530
533
|
try {
|
|
531
534
|
const entityService = config[action].service
|
|
532
535
|
|
|
@@ -575,13 +578,19 @@ const getServiceClient = async (baseEntity, action) => {
|
|
|
575
578
|
|
|
576
579
|
try {
|
|
577
580
|
const serviceClient = await soap.createClientAsync(urlToWsdl, wsdlOptions)
|
|
578
|
-
|
|
581
|
+
if (ctx?.request?.header?.authorization) { // Auth PassThrough
|
|
582
|
+
const [user, secret] = getCtxAuth(ctx)
|
|
583
|
+
serviceClient.setSecurity(new soap.WSSecurity(user || endpointUsername, secret, { passwordType: 'PasswordText', hasTimeStamp: false })) // ForwardInc using WSSecurity
|
|
584
|
+
} else {
|
|
585
|
+
serviceClient.setSecurity(new soap.WSSecurity(endpointUsername, endpointPassword, { passwordType: 'PasswordText', hasTimeStamp: false })) // ForwardInc using WSSecurity
|
|
586
|
+
}
|
|
579
587
|
serviceClient.addSoapHeader(customHeader)
|
|
580
588
|
serviceClient.setEndpoint(serviceEndpoint) // https://FQDN/path/to/service (wsdl name without ?wsdl extension)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
589
|
+
if (!ctx?.request?.header?.authorization) { // not using Auth PassThrough, store serviceClient and will be reused on subsequent requests
|
|
590
|
+
if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
|
|
591
|
+
_serviceClient[baseEntity][entityService] = serviceClient // serviceClient created
|
|
592
|
+
}
|
|
593
|
+
return serviceClient
|
|
585
594
|
} catch (err) {
|
|
586
595
|
if (err.message) throw new Error(`createClient ${urlToWsdl} errorMessage: ${err.message}`)
|
|
587
596
|
else throw new Error(`createClient ${urlToWsdl} errorMessage: invalid service definition - wsdl maybe not found?`)
|
|
@@ -590,7 +599,19 @@ const getServiceClient = async (baseEntity, action) => {
|
|
|
590
599
|
const newErr = err
|
|
591
600
|
throw newErr
|
|
592
601
|
}
|
|
593
|
-
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
//
|
|
605
|
+
// getCtxAuth returns username/secret from ctx header when using Auth PassThrough
|
|
606
|
+
//
|
|
607
|
+
const getCtxAuth = (ctx) => {
|
|
608
|
+
if (!ctx?.request?.header?.authorization) return []
|
|
609
|
+
const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
|
|
610
|
+
let username, password
|
|
611
|
+
if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
|
|
612
|
+
if (username) return [username, password] // basic auth
|
|
613
|
+
else return [undefined, authToken] // bearer auth
|
|
614
|
+
}
|
|
594
615
|
|
|
595
616
|
//
|
|
596
617
|
// Cleanup on exit
|
package/lib/plugin-ldap.js
CHANGED
|
@@ -225,7 +225,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
225
225
|
if (!ldapOptions) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
|
226
226
|
|
|
227
227
|
try {
|
|
228
|
-
const users = await doRequest(baseEntity, method, base, ldapOptions) // ignoring SCIM paging startIndex/count - get all
|
|
228
|
+
const users = await doRequest(baseEntity, method, base, ldapOptions, ctx) // ignoring SCIM paging startIndex/count - get all
|
|
229
229
|
result.totalResults = users.length
|
|
230
230
|
result.Resources = await Promise.all(users.map(async (user) => { // Promise.all because of async map
|
|
231
231
|
if (user.name) delete user.name // because mapper converts to SCIM name.xxx
|
|
@@ -245,13 +245,13 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
245
245
|
try {
|
|
246
246
|
if (Array.isArray(user.memberOf)) {
|
|
247
247
|
for (let i = 0; i < user.memberOf.length; i++) {
|
|
248
|
-
const id = await dnToSidGuid(baseEntity, user.memberOf[i])
|
|
248
|
+
const id = await dnToSidGuid(baseEntity, user.memberOf[i], ctx)
|
|
249
249
|
if (!id) throw new Error(`dnToGuid did not return any objectGUID value for dn=${user.memberOf[i]}`)
|
|
250
250
|
arr.push(id)
|
|
251
251
|
}
|
|
252
252
|
user.memberOf = arr
|
|
253
253
|
} else {
|
|
254
|
-
const id = await dnToSidGuid(baseEntity, user.memberOf)
|
|
254
|
+
const id = await dnToSidGuid(baseEntity, user.memberOf, ctx)
|
|
255
255
|
if (!id) throw new Error(`dnToGuid did not return any objectGUID value for dn=${user.memberOf}`)
|
|
256
256
|
user.memberOf = [id]
|
|
257
257
|
}
|
|
@@ -322,7 +322,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
|
|
|
322
322
|
const ldapOptions = endpointObj
|
|
323
323
|
|
|
324
324
|
try {
|
|
325
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
325
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
326
326
|
return null
|
|
327
327
|
} catch (err) {
|
|
328
328
|
const newErr = new Error(`${action} error: ${err.message}`)
|
|
@@ -351,7 +351,7 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
|
|
|
351
351
|
const ldapOptions = {}
|
|
352
352
|
|
|
353
353
|
try {
|
|
354
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
354
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
355
355
|
return null
|
|
356
356
|
} catch (err) {
|
|
357
357
|
throw new Error(`${action} error: ${err.message}`)
|
|
@@ -403,14 +403,14 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
403
403
|
operation: 'add',
|
|
404
404
|
modification: body.add
|
|
405
405
|
}
|
|
406
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
406
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
407
407
|
}
|
|
408
408
|
if (body.remove[groupsAttr].length > 0) {
|
|
409
409
|
const ldapOptions = {
|
|
410
410
|
operation: 'delete',
|
|
411
411
|
modification: body.remove
|
|
412
412
|
}
|
|
413
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
413
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
414
414
|
}
|
|
415
415
|
} catch (err) {
|
|
416
416
|
throw new Error(`${action} error: ${err.message}`)
|
|
@@ -442,7 +442,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
442
442
|
attributes: activeAttr
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
const users = await doRequest(baseEntity, method, base, ldapOptions)
|
|
445
|
+
const users = await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
446
446
|
if (users.length === 0) throw new Error(`${action} error: ${id} not found`)
|
|
447
447
|
else if (users.length > 1) throw new Error(`${action} error: ${ldapOptions.filter} returned more than one user for ${id}`)
|
|
448
448
|
const usr = users[0]
|
|
@@ -480,7 +480,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
try {
|
|
483
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
483
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
484
484
|
return null
|
|
485
485
|
} catch (err) {
|
|
486
486
|
throw new Error(`${action} error: ${err.message}`)
|
|
@@ -601,22 +601,22 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
601
601
|
if (!ldapOptions) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
|
602
602
|
|
|
603
603
|
try {
|
|
604
|
-
if (ldapOptions === 'getMemberOfGroups') result.Resources = await getMemberOfGroups(baseEntity, getObj.value)
|
|
604
|
+
if (ldapOptions === 'getMemberOfGroups') result.Resources = await getMemberOfGroups(baseEntity, getObj.value, ctx)
|
|
605
605
|
else {
|
|
606
|
-
const groups = await doRequest(baseEntity, method, base, ldapOptions)
|
|
606
|
+
const groups = await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
607
607
|
result.Resources = await Promise.all(groups.map(async (group) => { // Promise.all because of async map
|
|
608
608
|
if (config.useSID_id || config.useGUID_id) {
|
|
609
609
|
if (group.member) {
|
|
610
610
|
const arr = []
|
|
611
611
|
if (Array.isArray(group.member)) {
|
|
612
612
|
for (let i = 0; i < group.member.length; i++) {
|
|
613
|
-
const id = await dnToSidGuid(baseEntity, group.member[i])
|
|
613
|
+
const id = await dnToSidGuid(baseEntity, group.member[i], ctx)
|
|
614
614
|
if (!id) throw new Error(`dnToSidGuid() did not return any ${config.useSID_id ? 'objectSid' : 'objectGUID'} value for dn=${group.member[i]}`)
|
|
615
615
|
arr.push(id)
|
|
616
616
|
}
|
|
617
617
|
group.member = arr
|
|
618
618
|
} else {
|
|
619
|
-
const id = await dnToSidGuid(baseEntity, group.member)
|
|
619
|
+
const id = await dnToSidGuid(baseEntity, group.member, ctx)
|
|
620
620
|
if (!id) throw new Error(`dnToSidGuid() did not return any ${config.useSID_id ? 'objectSid' : 'objectGUID'} value for ${group.member}`)
|
|
621
621
|
group.member = [id]
|
|
622
622
|
}
|
|
@@ -654,7 +654,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
|
|
|
654
654
|
const ldapOptions = endpointObj
|
|
655
655
|
|
|
656
656
|
try {
|
|
657
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
657
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
658
658
|
return null
|
|
659
659
|
} catch (err) {
|
|
660
660
|
const newErr = new Error(`${action} error: ${err.message}`)
|
|
@@ -684,7 +684,7 @@ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
|
|
|
684
684
|
const ldapOptions = {}
|
|
685
685
|
|
|
686
686
|
try {
|
|
687
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
687
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
688
688
|
return null
|
|
689
689
|
} catch (err) {
|
|
690
690
|
throw new Error(`${action} error: ${err.message}`)
|
|
@@ -716,7 +716,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
716
716
|
for (let i = 0; i < attrObj.members.length; i++) {
|
|
717
717
|
const el = attrObj.members[i]
|
|
718
718
|
if (config.useSID_id || config.useGUID_id) {
|
|
719
|
-
const dn = await sidGuidToDn(baseEntity, el.value)
|
|
719
|
+
const dn = await sidGuidToDn(baseEntity, el.value, ctx)
|
|
720
720
|
if (!dn) throw new Error(`${action} error: sidGuidToDn() did not return any objectGUID value for dn=${el.value}`)
|
|
721
721
|
el.value = dn
|
|
722
722
|
}
|
|
@@ -739,14 +739,14 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
739
739
|
operation: 'add',
|
|
740
740
|
modification: body.add
|
|
741
741
|
}
|
|
742
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
742
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
743
743
|
}
|
|
744
744
|
if (body.remove[memberAttr].length > 0) {
|
|
745
745
|
const ldapOptions = { // using ldap lookup (dn) instead of search
|
|
746
746
|
operation: 'delete',
|
|
747
747
|
modification: body.remove
|
|
748
748
|
}
|
|
749
|
-
await doRequest(baseEntity, method, base, ldapOptions)
|
|
749
|
+
await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
750
750
|
}
|
|
751
751
|
return null
|
|
752
752
|
} catch (err) {
|
|
@@ -781,7 +781,7 @@ const getObjClassFilter = (baseEntity, type) => {
|
|
|
781
781
|
//
|
|
782
782
|
// dnToSidGuid is used for Active Directory to return objectGUID based on dn
|
|
783
783
|
//
|
|
784
|
-
const dnToSidGuid = async (baseEntity, dn) => {
|
|
784
|
+
const dnToSidGuid = async (baseEntity, dn, ctx) => {
|
|
785
785
|
const method = 'search'
|
|
786
786
|
const ldapOptions = {}
|
|
787
787
|
if (config.useSID_id) ldapOptions.attributes = ['objectSid']
|
|
@@ -790,7 +790,7 @@ const dnToSidGuid = async (baseEntity, dn) => {
|
|
|
790
790
|
|
|
791
791
|
try {
|
|
792
792
|
const base = dn
|
|
793
|
-
const objects = await doRequest(baseEntity, method, base, ldapOptions)
|
|
793
|
+
const objects = await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
794
794
|
if (objects.length !== 1) throw new Error(`did not find unique object having dn=${base}`)
|
|
795
795
|
if (config.useSID_id) return objects[0].objectSid
|
|
796
796
|
else return objects[0].objectGUID
|
|
@@ -803,7 +803,7 @@ const dnToSidGuid = async (baseEntity, dn) => {
|
|
|
803
803
|
//
|
|
804
804
|
// guidToDn is used for Active Directory to return dn based on objectGUID
|
|
805
805
|
//
|
|
806
|
-
const sidGuidToDn = async (baseEntity, id) => {
|
|
806
|
+
const sidGuidToDn = async (baseEntity, id, ctx) => {
|
|
807
807
|
const method = 'search'
|
|
808
808
|
const ldapOptions = {
|
|
809
809
|
attributes: ['dn']
|
|
@@ -818,7 +818,7 @@ const sidGuidToDn = async (baseEntity, id) => {
|
|
|
818
818
|
const guid = Buffer.from(id, 'base64').toString('hex')
|
|
819
819
|
base = `<GUID=${guid}>`
|
|
820
820
|
} else throw new Error('invalid call to sidGuidToDn(), configuration not using objectSid or objectGUID')
|
|
821
|
-
const objects = await doRequest(baseEntity, method, base, ldapOptions)
|
|
821
|
+
const objects = await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
822
822
|
if (objects.length !== 1) throw new Error(`did not find unique object having ${config.useSID_id ? 'objectSid' : 'objectGUID'} =${id}`)
|
|
823
823
|
return objects[0].dn
|
|
824
824
|
} catch (err) {
|
|
@@ -900,7 +900,7 @@ const convertStringToSid = (sidStr) => {
|
|
|
900
900
|
// getMemberOfGroups returns all groups the user is member of
|
|
901
901
|
// [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
|
|
902
902
|
//
|
|
903
|
-
const getMemberOfGroups = async (baseEntity, id) => {
|
|
903
|
+
const getMemberOfGroups = async (baseEntity, id, ctx) => {
|
|
904
904
|
const action = 'getMemberOfGroups'
|
|
905
905
|
if (!config.map.group) throw new Error('missing configuration endpoint.map.group') // not using groups
|
|
906
906
|
|
|
@@ -922,7 +922,7 @@ const getMemberOfGroups = async (baseEntity, id) => {
|
|
|
922
922
|
}
|
|
923
923
|
|
|
924
924
|
try {
|
|
925
|
-
const users = await doRequest(baseEntity, method, base, ldapOptions)
|
|
925
|
+
const users = await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
926
926
|
if (users.length !== 1) throw new Error(`${action} error: did not find unique user having ${config.useSID_id ? 'objectSid' : 'objectGUID'} =${id}`)
|
|
927
927
|
idDn = users[0].dn
|
|
928
928
|
} catch (err) {
|
|
@@ -948,7 +948,7 @@ const getMemberOfGroups = async (baseEntity, id) => {
|
|
|
948
948
|
}
|
|
949
949
|
|
|
950
950
|
try {
|
|
951
|
-
const groups = await doRequest(baseEntity, method, base, ldapOptions)
|
|
951
|
+
const groups = await doRequest(baseEntity, method, base, ldapOptions, ctx)
|
|
952
952
|
return groups.map((grp) => {
|
|
953
953
|
return { // { id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }
|
|
954
954
|
id: encodeURIComponent(grp[attrs[0]]), // not mandatory, but included anyhow
|
|
@@ -962,10 +962,22 @@ const getMemberOfGroups = async (baseEntity, id) => {
|
|
|
962
962
|
}
|
|
963
963
|
}
|
|
964
964
|
|
|
965
|
+
//
|
|
966
|
+
// getCtxAuth returns username/secret from ctx header when using Auth PassThrough
|
|
967
|
+
//
|
|
968
|
+
const getCtxAuth = (ctx) => { // eslint-disable-line
|
|
969
|
+
if (!ctx?.request?.header?.authorization) return []
|
|
970
|
+
const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
|
|
971
|
+
let username, password
|
|
972
|
+
if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
|
|
973
|
+
if (username) return [username, password] // basic auth
|
|
974
|
+
else return [undefined, authToken] // bearer auth
|
|
975
|
+
}
|
|
976
|
+
|
|
965
977
|
//
|
|
966
978
|
// getServiceClient returns LDAP client used by doRequest
|
|
967
979
|
//
|
|
968
|
-
const getServiceClient = async (baseEntity) => {
|
|
980
|
+
const getServiceClient = async (baseEntity, ctx) => {
|
|
969
981
|
const action = 'getServiceClient'
|
|
970
982
|
if (!config.entity[baseEntity].passwordDecrypted) config.entity[baseEntity].passwordDecrypted = scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)
|
|
971
983
|
if (!config.entity[baseEntity].baseUrl) config.entity[baseEntity].baseUrl = config.entity[baseEntity].baseUrls[0] // failover logic also updates baseUrl
|
|
@@ -983,7 +995,11 @@ const getServiceClient = async (baseEntity) => {
|
|
|
983
995
|
strictDN: false // false => allows none standard ldap base dn e.g. <SID=...> / <GUID=...> ref. objectSid/objectGUID
|
|
984
996
|
})
|
|
985
997
|
await new Promise((resolve, reject) => {
|
|
986
|
-
|
|
998
|
+
if (ctx?.request?.header?.authorization) { // using ctx authentication PassThrough
|
|
999
|
+
const [username, password] = getCtxAuth(ctx)
|
|
1000
|
+
if (username) cli.bind(username, password, (err, res) => err ? reject(err) : resolve(res)) // basic auth
|
|
1001
|
+
else cli.bind(config.entity[baseEntity].username, password, (err, res) => err ? reject(err) : resolve(res)) // bearer token, using username from configuration
|
|
1002
|
+
} else cli.bind(config.entity[baseEntity].username, config.entity[baseEntity].passwordDecrypted, (err, res) => err ? reject(err) : resolve(res))
|
|
987
1003
|
cli.on('error', (err) => reject(err))
|
|
988
1004
|
})
|
|
989
1005
|
return cli // client OK
|
|
@@ -1013,7 +1029,7 @@ const getServiceClient = async (baseEntity) => {
|
|
|
1013
1029
|
// "attributes": ["sAMAccountName","displayName","mail"]
|
|
1014
1030
|
// }
|
|
1015
1031
|
//
|
|
1016
|
-
const doRequest = async (baseEntity, method, base, ldapOptions) => {
|
|
1032
|
+
const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
|
|
1017
1033
|
let result = null
|
|
1018
1034
|
let client = null
|
|
1019
1035
|
|
|
@@ -1029,7 +1045,7 @@ const doRequest = async (baseEntity, method, base, ldapOptions) => {
|
|
|
1029
1045
|
}
|
|
1030
1046
|
|
|
1031
1047
|
try {
|
|
1032
|
-
client = await getServiceClient(baseEntity)
|
|
1048
|
+
client = await getServiceClient(baseEntity, ctx)
|
|
1033
1049
|
switch (method) {
|
|
1034
1050
|
case 'search':
|
|
1035
1051
|
options.paged = { pageSize: 200, pagePause: false } // parse entire directory calling 'page' method for each page
|
package/lib/plugin-loki.js
CHANGED
|
@@ -267,27 +267,27 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
267
267
|
const userObj = res[0]
|
|
268
268
|
|
|
269
269
|
for (const key in attrObj) {
|
|
270
|
-
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups)
|
|
271
|
-
attrObj[key].
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
270
|
+
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
|
|
271
|
+
const delArr = attrObj[key].filter(el => el.operation === 'delete')
|
|
272
|
+
const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
|
|
273
|
+
if (!userObj[key]) userObj[key] = []
|
|
274
|
+
// delete
|
|
275
|
+
userObj[key] = userObj[key].filter(el => {
|
|
276
|
+
if (delArr.findIndex(e => e.value === el.value) >= 0) return false
|
|
277
|
+
return true
|
|
278
|
+
})
|
|
279
|
+
// add
|
|
280
|
+
addArr.forEach(el => {
|
|
281
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
282
|
+
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
283
|
+
const index = userObj[key].findIndex(e => e.primary === el.primary)
|
|
284
|
+
if (index >= 0) {
|
|
285
|
+
if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
|
|
286
|
+
else userObj[key][index].primary = undefined // remove primary attribute, only one primary
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
|
-
userObj[key].push(el)
|
|
290
289
|
}
|
|
290
|
+
userObj[key].push(el)
|
|
291
291
|
})
|
|
292
292
|
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
293
293
|
if (!attrObj[key]) delete userObj[key] // blank or null
|
|
@@ -300,6 +300,15 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
300
300
|
if (userObj[key].length < 1) delete userObj[key]
|
|
301
301
|
} else { // modify/create multivalue
|
|
302
302
|
if (!userObj[key]) userObj[key] = []
|
|
303
|
+
if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
|
|
304
|
+
const primVal = attrObj[key][el].primary
|
|
305
|
+
if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
|
|
306
|
+
const index = userObj[key].findIndex(e => e.primary === primVal)
|
|
307
|
+
if (index >= 0) {
|
|
308
|
+
userObj[key][index].primary = undefined
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
303
312
|
const found = userObj[key].find((e, i) => {
|
|
304
313
|
if (e.type === el || (!e.type && el === 'undefined')) {
|
|
305
314
|
for (const k in attrObj[key][el]) {
|
package/lib/plugin-mongodb.js
CHANGED
|
@@ -49,20 +49,34 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
|
|
|
49
49
|
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']
|
|
50
50
|
|
|
51
51
|
if (!config.entity) throw new Error('error: configuration entity is missing')
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (arr.length !== 2 || arr[0] !== 'mongodb:') throw new Error('error: configuration baseUrls is not using expected format mongodb://hostname:port')
|
|
56
|
-
const username = config.entity[baseEntity].username
|
|
57
|
-
const password = scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)
|
|
58
|
-
const dbConn = `${arr[0]}//${encodeURIComponent(username)}:${encodeURIComponent(password)}@${arr[1]}` // percent encoded username/password
|
|
59
|
-
const client = new MongoClient(dbConn, { useUnifiedTopology: true })
|
|
60
|
-
loadHandler(baseEntity, client)
|
|
52
|
+
if (!scimgateway.authPassThroughAllowed) { // not using Auth PassThrough, loading db handler at startup using username/password from config
|
|
53
|
+
for (const baseEntity in config.entity) {
|
|
54
|
+
loadHandler(baseEntity)
|
|
61
55
|
}
|
|
62
56
|
}
|
|
63
57
|
|
|
64
|
-
async function loadHandler (baseEntity,
|
|
58
|
+
async function loadHandler (baseEntity, ctx) {
|
|
65
59
|
const action = 'loadHander'
|
|
60
|
+
if (!config.entity[baseEntity].baseUrl) { // mongodb://host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]] - e.g: mongodb://localhost:27017/db?tls=true&tlsInsecure=true
|
|
61
|
+
throw new Error(`${action} error: configuration entity.${baseEntity}.baseUrl is missing`)
|
|
62
|
+
}
|
|
63
|
+
const arr = config.entity[baseEntity].baseUrl.split('//')
|
|
64
|
+
if (arr.length !== 2 || arr[0] !== 'mongodb:') throw new Error('error: configuration baseUrls is not using expected format mongodb://hostname:port')
|
|
65
|
+
|
|
66
|
+
let username
|
|
67
|
+
let password
|
|
68
|
+
if (ctx?.request?.header?.authorization) { // Auth PassThrough
|
|
69
|
+
const [user, secret] = getCtxAuth(ctx)
|
|
70
|
+
if (user) username = user
|
|
71
|
+
else username = config.entity[baseEntity].username // bearer token, using username from configuration
|
|
72
|
+
password = secret
|
|
73
|
+
} else {
|
|
74
|
+
username = config.entity[baseEntity].username
|
|
75
|
+
password = scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)
|
|
76
|
+
}
|
|
77
|
+
const dbConn = `${arr[0]}//${encodeURIComponent(username)}:${encodeURIComponent(password)}@${arr[1]}` // percent encoded username/password
|
|
78
|
+
const client = new MongoClient(dbConn, { useUnifiedTopology: true })
|
|
79
|
+
|
|
66
80
|
const dbName = config.entity[baseEntity].database ? config.entity[baseEntity].database : 'scim'
|
|
67
81
|
let db
|
|
68
82
|
let users
|
|
@@ -72,7 +86,9 @@ async function loadHandler (baseEntity, client) {
|
|
|
72
86
|
await client.connect()
|
|
73
87
|
db = await client.db(dbName)
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
const clientIdentifier = getClientIdentifier(ctx)
|
|
90
|
+
if (!config.entity[baseEntity][clientIdentifier]) config.entity[baseEntity][clientIdentifier] = {}
|
|
91
|
+
config.entity[baseEntity][clientIdentifier].client = client
|
|
76
92
|
config.entity[baseEntity].db = db
|
|
77
93
|
|
|
78
94
|
if (await isMongoCollection(baseEntity, 'users')) users = await db.collection('users')
|
|
@@ -131,10 +147,11 @@ async function loadHandler (baseEntity, client) {
|
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
149
|
}
|
|
134
|
-
|
|
135
|
-
config.entity[baseEntity].
|
|
136
|
-
config.entity[baseEntity].collection
|
|
137
|
-
config.entity[baseEntity].collection.
|
|
150
|
+
const clientIdentifier = getClientIdentifier(ctx)
|
|
151
|
+
if (!config.entity[baseEntity][clientIdentifier]) config.entity[baseEntity][clientIdentifier] = {}
|
|
152
|
+
config.entity[baseEntity][clientIdentifier].collection = {}
|
|
153
|
+
config.entity[baseEntity][clientIdentifier].collection.users = users
|
|
154
|
+
config.entity[baseEntity][clientIdentifier].collection.groups = groups
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
// =================================================
|
|
@@ -186,7 +203,11 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
186
203
|
}
|
|
187
204
|
}
|
|
188
205
|
|
|
189
|
-
const
|
|
206
|
+
const clientIdentifier = getClientIdentifier(ctx)
|
|
207
|
+
if (ctx && !config.entity[baseEntity][clientIdentifier]) { // first (or previous failed) PassThrough attempt - have to load connection
|
|
208
|
+
await loadHandler(baseEntity, ctx)
|
|
209
|
+
}
|
|
210
|
+
const users = config.entity[baseEntity][clientIdentifier].collection.users
|
|
190
211
|
let findObj
|
|
191
212
|
|
|
192
213
|
// mandatory if-else logic - start
|
|
@@ -353,27 +374,27 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
353
374
|
let userObj = decodeDotDate(res[0])
|
|
354
375
|
|
|
355
376
|
for (const key in attrObj) {
|
|
356
|
-
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups)
|
|
357
|
-
attrObj[key].
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
377
|
+
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
|
|
378
|
+
const delArr = attrObj[key].filter(el => el.operation === 'delete')
|
|
379
|
+
const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
|
|
380
|
+
if (!userObj[key]) userObj[key] = []
|
|
381
|
+
// delete
|
|
382
|
+
userObj[key] = userObj[key].filter(el => {
|
|
383
|
+
if (delArr.findIndex(e => e.value === el.value) >= 0) return false
|
|
384
|
+
return true
|
|
385
|
+
})
|
|
386
|
+
// add
|
|
387
|
+
addArr.forEach(el => {
|
|
388
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
389
|
+
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
390
|
+
const index = userObj[key].findIndex(e => e.primary === el.primary)
|
|
391
|
+
if (index >= 0) {
|
|
392
|
+
if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
|
|
393
|
+
else userObj[key][index].primary = undefined // remove primary attribute, only one primary
|
|
373
394
|
}
|
|
374
395
|
}
|
|
375
|
-
userObj[key].push(el)
|
|
376
396
|
}
|
|
397
|
+
userObj[key].push(el)
|
|
377
398
|
})
|
|
378
399
|
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
379
400
|
if (!attrObj[key]) delete userObj[key] // blank or null
|
|
@@ -386,6 +407,15 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
386
407
|
if (userObj[key].length < 1) delete userObj[key]
|
|
387
408
|
} else { // modify/create multivalue
|
|
388
409
|
if (!userObj[key]) userObj[key] = []
|
|
410
|
+
if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
|
|
411
|
+
const primVal = attrObj[key][el].primary
|
|
412
|
+
if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
|
|
413
|
+
const index = userObj[key].findIndex(e => e.primary === primVal)
|
|
414
|
+
if (index >= 0) {
|
|
415
|
+
userObj[key][index].primary = undefined
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
389
419
|
const found = userObj[key].find((e, i) => {
|
|
390
420
|
if (e.type === el || (!e.type && el === 'undefined')) {
|
|
391
421
|
for (const k in attrObj[key][el]) {
|
|
@@ -709,11 +739,30 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
709
739
|
// =================================================
|
|
710
740
|
// helpers
|
|
711
741
|
// =================================================
|
|
742
|
+
|
|
743
|
+
const getClientIdentifier = (ctx) => {
|
|
744
|
+
if (!ctx?.request?.header?.authorization) return undefined
|
|
745
|
+
const [user, secret] = getCtxAuth(ctx)
|
|
746
|
+
return `${encodeURIComponent(user)}_${encodeURIComponent(secret)}` // user_password or undefined_password
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
//
|
|
750
|
+
// getCtxAuth returns username/secret from ctx header when using Auth PassThrough
|
|
751
|
+
//
|
|
752
|
+
const getCtxAuth = (ctx) => {
|
|
753
|
+
if (!ctx?.request?.header?.authorization) return []
|
|
754
|
+
const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
|
|
755
|
+
let username, password
|
|
756
|
+
if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
|
|
757
|
+
if (username) return [username, password] // basic auth
|
|
758
|
+
else return [undefined, authToken] // bearer auth
|
|
759
|
+
}
|
|
760
|
+
|
|
712
761
|
const decodeDotDate = (obj) => { // replace dot with unicode
|
|
713
762
|
const retObj = JSON.parse(JSON.stringify(obj)) // new object - don't modify source
|
|
714
763
|
Object.keys(retObj).forEach(function (key) {
|
|
715
764
|
if (key.includes('·')) {
|
|
716
|
-
retObj[key.replace(/\·/g, '.')] = retObj[key]
|
|
765
|
+
retObj[key.replace(/\·/g, '.')] = retObj[key] // eslint-disable-line
|
|
717
766
|
delete retObj[key]
|
|
718
767
|
}
|
|
719
768
|
})
|
|
@@ -785,12 +834,20 @@ async function dropMongoCollection (baseEntity, collection) {
|
|
|
785
834
|
process.on('SIGTERM', () => {
|
|
786
835
|
// kill
|
|
787
836
|
for (const baseEntity in config.entity) {
|
|
788
|
-
|
|
837
|
+
for (const key in config.entity[baseEntity]) {
|
|
838
|
+
if (config.entity[baseEntity][key].client) {
|
|
839
|
+
config.entity[baseEntity][key].client.close()
|
|
840
|
+
}
|
|
841
|
+
}
|
|
789
842
|
}
|
|
790
843
|
})
|
|
791
844
|
process.on('SIGINT', () => {
|
|
792
845
|
// Ctrl+C
|
|
793
846
|
for (const baseEntity in config.entity) {
|
|
794
|
-
|
|
847
|
+
for (const key in config.entity[baseEntity]) {
|
|
848
|
+
if (config.entity[baseEntity][key].client) {
|
|
849
|
+
config.entity[baseEntity][key].client.close()
|
|
850
|
+
}
|
|
851
|
+
}
|
|
795
852
|
}
|
|
796
853
|
})
|