scimgateway 6.1.17 → 6.1.18
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 +7 -0
- package/lib/azure-license-mapping.json +13 -0
- package/lib/plugin-entra-id.ts +1 -1
- package/lib/plugin-loki.ts +4 -110
- package/lib/plugin-mongodb.ts +1 -115
- package/lib/scimgateway.ts +93 -8
- package/lib/utils-scim.ts +120 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -801,6 +801,7 @@ There are two options: run SCIM Gateway in a single image, or use Docker Compose
|
|
|
801
801
|
bun install scimgateway
|
|
802
802
|
bun pm trust scimgateway
|
|
803
803
|
cp ./config/docker/* .
|
|
804
|
+
cp ./config/docker/.dockerignore .
|
|
804
805
|
```
|
|
805
806
|
|
|
806
807
|
**Dockerfile** <== Main dockerfile
|
|
@@ -1303,6 +1304,12 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1303
1304
|
|
|
1304
1305
|
## Change log
|
|
1305
1306
|
|
|
1307
|
+
### v6.1.18
|
|
1308
|
+
|
|
1309
|
+
[Fixed]
|
|
1310
|
+
|
|
1311
|
+
- createUser and modifyUser returns full user object. Some endpoints like Entra ID hasn’t caught up yet due to internal sync and changes are not reflected. This update ensure returned user object contains what have been modified.
|
|
1312
|
+
|
|
1306
1313
|
### v6.1.17
|
|
1307
1314
|
|
|
1308
1315
|
[Fixed]
|
|
@@ -19,6 +19,19 @@
|
|
|
19
19
|
"O365_w/o_Teams_Bundle_M5":{"displayName":"Office 365 without Teams Bundle M5","category":"Microsoft 365","licenseCategory":"Paid","isBillable":true,"priceUSD":28,"includes":["ENTERPRISEPACK","EXCHANGESTANDARD","SHAREPOINTSTANDARD"],"derivedIncludes":["EXCHANGEENTERPRISE","SHAREPOINTENTERPRISE","MCOSTANDARD","ONEDRIVESTANDARD","INTUNE_A","AAD_PREMIUM","EXCHANGESTANDARD","SHAREPOINTSTANDARD"]},
|
|
20
20
|
"POWER_BI_PREMIUM_PER_USER":{"displayName":"Power BI Premium Per User","category":"Power Platform","licenseCategory":"Paid","isBillable":true,"priceUSD":20,"includes":["POWER_BI_PRO"],"derivedIncludes":["POWER_BI_PRO"]},
|
|
21
21
|
"RMS_S_PREMIUM":{"displayName":"Azure Information Protection Premium P2","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":9,"includes":["RMS_S_ENTERPRISE"],"derivedIncludes":["RMS_S_ENTERPRISE"]},
|
|
22
|
+
"O365_BUSINESS_ESSENTIALS":{"displayName":"Microsoft 365 Business Basic","category":"Microsoft 365","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"],"derivedIncludes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"]},
|
|
23
|
+
"M365_BUSINESS_BASIC":{"displayName":"Microsoft 365 Business Basic","category":"Microsoft 365","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"],"derivedIncludes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"]},
|
|
24
|
+
"M365_BUSINESS_STANDARD":{"displayName":"Microsoft 365 Business Standard","category":"Microsoft 365","licenseCategory":"Paid","isBillable":true,"priceUSD":12,"includes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"],"derivedIncludes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"]},
|
|
25
|
+
"M365_BUSINESS_PREMIUM":{"displayName":"Microsoft 365 Business Premium","category":"Microsoft 365","licenseCategory":"Paid","isBillable":true,"priceUSD":22,"includes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD","INTUNE_A","AAD_PREMIUM"],"derivedIncludes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD","INTUNE_A","AAD_PREMIUM"]},
|
|
26
|
+
"ENTRA_ID_GOVERNANCE":{"displayName":"Microsoft Entra ID Governance","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":7,"includes":["AAD_PREMIUM_P2"],"derivedIncludes":["AAD_PREMIUM_P2","AAD_PREMIUM"]},
|
|
27
|
+
"Microsoft_365_F1":{"displayName":"Microsoft 365 F1","category":"Microsoft 365","licenseCategory":"Paid","isBillable":true,"priceUSD":2.25,"includes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"],"derivedIncludes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"]},
|
|
28
|
+
"ENTRA_IDENTITY_PROTECTION":{"displayName":"Microsoft Entra ID Protection","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":["AAD_PREMIUM_P2"],"derivedIncludes":["AAD_PREMIUM_P2","AAD_PREMIUM"]},
|
|
29
|
+
"ENTRA_IDENTITY_GOVERNANCE_STEPUP":{"displayName":"Microsoft Entra ID Governance Step-Up","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":4,"includes":["AAD_PREMIUM_P1"],"derivedIncludes":["AAD_PREMIUM_P1"]},
|
|
30
|
+
"ENTRA_PERMISSIONS_MANAGEMENT":{"displayName":"Microsoft Entra Permissions Management","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":10,"includes":[]},
|
|
31
|
+
"AAD_PREMIUM_P1":{"displayName":"Microsoft Entra ID P1","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":[]},
|
|
32
|
+
"AAD_B2C":{"displayName":"Azure AD B2C","category":"Entra","licenseCategory":"Consumption","isBillable":true,"priceUSD":0,"includes":[]},
|
|
33
|
+
"AAD_BASIC_EDU":{"displayName":"Azure Active Directory Basic (Education)","category":"Entra","licenseCategory":"Free","isBillable":false,"priceUSD":0,"includes":[]},
|
|
34
|
+
"M365_DEFENDER_IDENTITY":{"displayName":"Microsoft 365 Defender for Identity","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":[]},
|
|
22
35
|
"Microsoft_Teams_EEA_New": { "displayName": "Microsoft Teams EEA New", "category": "Collaboration", "licenseCategory": "Paid", "isBillable": true, "priceUSD": 4.00, "includes": [] },
|
|
23
36
|
"POWERAPPS_DEV": { "displayName": "Power Apps Developer Plan", "category": "Power Platform", "licenseCategory": "Free", "isBillable": false, "priceUSD": 0.00, "includes": [] },
|
|
24
37
|
"EXCHANGESTANDARD":{"displayName":"Exchange Online Plan 1","category":"Exchange","licenseCategory":"Paid","isBillable":true,"priceUSD":4,"includes":[]},
|
package/lib/plugin-entra-id.ts
CHANGED
|
@@ -822,7 +822,7 @@ scimgateway.getEntitlements = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
822
822
|
throw new Error(`${action} error: advanced filtering not supported: ${getObj.rawFilter}`)
|
|
823
823
|
} else {
|
|
824
824
|
// mandatory - no filtering
|
|
825
|
-
path =
|
|
825
|
+
path = '/subscribedSkus'
|
|
826
826
|
}
|
|
827
827
|
|
|
828
828
|
if (!path) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
package/lib/plugin-loki.ts
CHANGED
|
@@ -281,116 +281,10 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
281
281
|
const res = users.find({ id: id })
|
|
282
282
|
if (res.length === 0) throw new Error(`${action} error: user id=${id} - user does not exist`)
|
|
283
283
|
if (res.length > 1) throw new Error(`${action} error: user id=${id} - user is not unique, more than one have been found`)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const delArr = attrObj[key].filter(el => el.operation === 'delete')
|
|
289
|
-
const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
|
|
290
|
-
if (!userObj[key] || !Array.isArray(userObj[key])) userObj[key] = []
|
|
291
|
-
// delete
|
|
292
|
-
userObj[key] = userObj[key].filter((el: Record<string, any>) => {
|
|
293
|
-
const index = delArr.findIndex((e) => {
|
|
294
|
-
let elExist = false
|
|
295
|
-
for (const k in e) {
|
|
296
|
-
if (k === 'primary' || k === 'operation') continue
|
|
297
|
-
if (e[k] !== el[k]) {
|
|
298
|
-
elExist = false
|
|
299
|
-
break
|
|
300
|
-
}
|
|
301
|
-
elExist = true
|
|
302
|
-
}
|
|
303
|
-
return elExist
|
|
304
|
-
})
|
|
305
|
-
if (index >= 0) return false
|
|
306
|
-
else return true
|
|
307
|
-
})
|
|
308
|
-
// add
|
|
309
|
-
addArr.forEach((el) => {
|
|
310
|
-
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
311
|
-
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
312
|
-
const index = userObj[key].findIndex((e: Record<string, any>) => e.primary === el.primary)
|
|
313
|
-
if (index >= 0) {
|
|
314
|
-
if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
|
|
315
|
-
else userObj[key][index].primary = undefined // remove primary attribute, only one primary
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
const index = userObj[key].findIndex((e: Record<string, any>, _index: number) => { // avoid adding existing
|
|
320
|
-
if (el.value && el.value === e.value && el.type === e.type) {
|
|
321
|
-
for (const k in el) {
|
|
322
|
-
e[k] = el[k]
|
|
323
|
-
}
|
|
324
|
-
return true
|
|
325
|
-
}
|
|
326
|
-
let elExist = false
|
|
327
|
-
for (const k in el) {
|
|
328
|
-
if (el[k] !== e[k]) {
|
|
329
|
-
if (k === 'primary') continue
|
|
330
|
-
elExist = false
|
|
331
|
-
break
|
|
332
|
-
}
|
|
333
|
-
elExist = true
|
|
334
|
-
}
|
|
335
|
-
return elExist
|
|
336
|
-
})
|
|
337
|
-
if (index < 0) userObj[key].push(el)
|
|
338
|
-
})
|
|
339
|
-
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
340
|
-
if (!attrObj[key]) delete userObj[key] // blank or null
|
|
341
|
-
for (const el in attrObj[key]) {
|
|
342
|
-
attrObj[key][el].type = el
|
|
343
|
-
if (attrObj[key][el].operation && attrObj[key][el].operation === 'delete') { // delete multivalue
|
|
344
|
-
let type: any = el
|
|
345
|
-
if (type === 'undefined') type = undefined
|
|
346
|
-
userObj[key] = userObj[key].filter((e: Record<string, any>) => e.type !== type)
|
|
347
|
-
if (userObj[key].length < 1) delete userObj[key]
|
|
348
|
-
} else { // modify/create multivalue
|
|
349
|
-
if (!userObj[key]) userObj[key] = []
|
|
350
|
-
if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
|
|
351
|
-
const primVal = attrObj[key][el].primary
|
|
352
|
-
if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
|
|
353
|
-
const index = userObj[key].findIndex((e: Record<string, any>) => e.primary === primVal)
|
|
354
|
-
if (index >= 0) {
|
|
355
|
-
userObj[key][index].primary = undefined
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
const found = userObj[key].find((e: Record<string, any>, i: any) => {
|
|
360
|
-
if (e.type === el || (!e.type && el === 'undefined')) {
|
|
361
|
-
for (const k in attrObj[key][el]) {
|
|
362
|
-
userObj[key][i][k] = attrObj[key][el][k]
|
|
363
|
-
if (k === 'type' && attrObj[key][el][k] === 'undefined') delete userObj[key][i][k] // don't store with type "undefined"
|
|
364
|
-
}
|
|
365
|
-
return true
|
|
366
|
-
} else return false
|
|
367
|
-
})
|
|
368
|
-
if (attrObj[key][el].type && attrObj[key][el].type === 'undefined') delete attrObj[key][el].type // don't store with type "undefined"
|
|
369
|
-
if (!found) userObj[key].push(attrObj[key][el]) // create
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
} else {
|
|
373
|
-
// None multi value attribute, blank will be deleted
|
|
374
|
-
if (typeof (attrObj[key]) === 'object' && attrObj[key] !== null) {
|
|
375
|
-
// name.familyName=Bianchi
|
|
376
|
-
if (!userObj[key]) userObj[key] = {} // e.g name object does not exist
|
|
377
|
-
for (const sub in attrObj[key]) {
|
|
378
|
-
if (!userObj[key]) userObj[key] = {}
|
|
379
|
-
if (Object.prototype.hasOwnProperty.call(attrObj[key][sub], 'value')
|
|
380
|
-
&& attrObj[key][sub].value === '') delete userObj[key][sub] // object having blank value attribute e.g. {"manager": {"value": "",...}}
|
|
381
|
-
else if (attrObj[key][sub] === '') delete userObj[key][sub]
|
|
382
|
-
else {
|
|
383
|
-
if (!userObj[key]) userObj[key] = {} // may have been deleted by length check below
|
|
384
|
-
userObj[key][sub] = attrObj[key][sub]
|
|
385
|
-
}
|
|
386
|
-
if (Object.keys(userObj[key]).length < 1) delete userObj[key]
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
if (attrObj[key] === '') delete userObj[key]
|
|
390
|
-
else userObj[key] = attrObj[key]
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
284
|
+
|
|
285
|
+
let userObj: any = res[0]
|
|
286
|
+
userObj = scimgateway.patchObj(userObj, attrObj) // merge
|
|
287
|
+
|
|
394
288
|
await users.update(userObj) // needed for persistence
|
|
395
289
|
return null
|
|
396
290
|
}
|
package/lib/plugin-mongodb.ts
CHANGED
|
@@ -354,121 +354,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
let userObj = decodeDotDate(res[0])
|
|
357
|
-
|
|
358
|
-
for (const key in attrObj) {
|
|
359
|
-
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
|
|
360
|
-
const delArr = attrObj[key].filter(el => el.operation === 'delete')
|
|
361
|
-
const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
|
|
362
|
-
if (!userObj[key] || !Array.isArray(userObj[key])) userObj[key] = []
|
|
363
|
-
// delete
|
|
364
|
-
userObj[key] = userObj[key].filter((el: Record<string, any>) => {
|
|
365
|
-
const index = delArr.findIndex((e) => {
|
|
366
|
-
let elExist = false
|
|
367
|
-
for (const k in e) {
|
|
368
|
-
if (k === 'primary' || k === 'operation') continue
|
|
369
|
-
if (e[k] !== el[k]) {
|
|
370
|
-
elExist = false
|
|
371
|
-
break
|
|
372
|
-
}
|
|
373
|
-
elExist = true
|
|
374
|
-
}
|
|
375
|
-
return elExist
|
|
376
|
-
})
|
|
377
|
-
if (index >= 0) return false
|
|
378
|
-
else return true
|
|
379
|
-
})
|
|
380
|
-
// add
|
|
381
|
-
addArr.forEach((el) => {
|
|
382
|
-
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
383
|
-
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
384
|
-
const index = userObj[key].findIndex((e: Record<string, any>) => e.primary === el.primary)
|
|
385
|
-
if (index >= 0) {
|
|
386
|
-
if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
|
|
387
|
-
else userObj[key][index].primary = undefined // remove primary attribute, only one primary
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
const index = userObj[key].findIndex((e: Record<string, any>, _index: number) => { // avoid adding existing
|
|
392
|
-
if (el.value && el.value === e.value && el.type === e.type) {
|
|
393
|
-
for (const k in el) {
|
|
394
|
-
e[k] = el[k]
|
|
395
|
-
}
|
|
396
|
-
return true
|
|
397
|
-
}
|
|
398
|
-
let elExist = false
|
|
399
|
-
for (const k in el) {
|
|
400
|
-
if (el[k] !== e[k]) {
|
|
401
|
-
if (k === 'primary') continue
|
|
402
|
-
elExist = false
|
|
403
|
-
break
|
|
404
|
-
}
|
|
405
|
-
elExist = true
|
|
406
|
-
}
|
|
407
|
-
return elExist
|
|
408
|
-
})
|
|
409
|
-
if (index < 0) userObj[key].push(el)
|
|
410
|
-
})
|
|
411
|
-
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
412
|
-
if (!attrObj[key]) delete userObj[key] // blank or null
|
|
413
|
-
for (const el in attrObj[key]) {
|
|
414
|
-
attrObj[key][el].type = el
|
|
415
|
-
if (attrObj[key][el].operation && attrObj[key][el].operation === 'delete') { // delete multivalue
|
|
416
|
-
let type: any = el
|
|
417
|
-
if (type === 'undefined') type = undefined
|
|
418
|
-
userObj[key] = userObj[key].filter((e: Record<string, any>) => e.type !== type)
|
|
419
|
-
if (userObj[key].length < 1) delete userObj[key]
|
|
420
|
-
} else { // modify/create multivalue
|
|
421
|
-
if (!userObj[key]) userObj[key] = []
|
|
422
|
-
if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
|
|
423
|
-
const primVal = attrObj[key][el].primary
|
|
424
|
-
if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
|
|
425
|
-
const index = userObj[key].findIndex((e: Record<string, any>) => e.primary === primVal)
|
|
426
|
-
if (index >= 0) {
|
|
427
|
-
userObj[key][index].primary = undefined
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const found = userObj[key].find((e: Record<string, any>, i: any) => {
|
|
432
|
-
if (e.type === el || (!e.type && el === 'undefined')) {
|
|
433
|
-
for (const k in attrObj[key][el]) {
|
|
434
|
-
userObj[key][i][k] = attrObj[key][el][k]
|
|
435
|
-
if (k === 'type' && attrObj[key][el][k] === 'undefined') delete userObj[key][i][k] // don't store with type "undefined"
|
|
436
|
-
}
|
|
437
|
-
return true
|
|
438
|
-
} else return false
|
|
439
|
-
})
|
|
440
|
-
if (attrObj[key][el].type && attrObj[key][el].type === 'undefined') delete attrObj[key][el].type // don't store with type "undefined"
|
|
441
|
-
if (!found) userObj[key].push(attrObj[key][el]) // create
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
445
|
-
// None multi value attribute
|
|
446
|
-
if (typeof (attrObj[key]) !== 'object' || attrObj[key] === null) {
|
|
447
|
-
if (attrObj[key] === '' || attrObj[key] === null) delete userObj[key]
|
|
448
|
-
else userObj[key] = attrObj[key]
|
|
449
|
-
} else {
|
|
450
|
-
// None multi value attribute, blank will be deleted
|
|
451
|
-
if (typeof (attrObj[key]) === 'object' && attrObj[key] !== null) {
|
|
452
|
-
// name.familyName=Bianchi
|
|
453
|
-
if (!userObj[key]) userObj[key] = {} // e.g name object does not exist
|
|
454
|
-
for (const sub in attrObj[key]) {
|
|
455
|
-
if (!userObj[key]) userObj[key] = {}
|
|
456
|
-
if (Object.prototype.hasOwnProperty.call(attrObj[key][sub], 'value')
|
|
457
|
-
&& attrObj[key][sub].value === '') delete userObj[key][sub] // object having blank value attribute e.g. {"manager": {"value": "",...}}
|
|
458
|
-
else if (attrObj[key][sub] === '') delete userObj[key][sub]
|
|
459
|
-
else {
|
|
460
|
-
if (!userObj[key]) userObj[key] = {} // may have been deleted by length check below
|
|
461
|
-
userObj[key][sub] = attrObj[key][sub]
|
|
462
|
-
}
|
|
463
|
-
if (Object.keys(userObj[key]).length < 1) delete userObj[key]
|
|
464
|
-
}
|
|
465
|
-
} else {
|
|
466
|
-
if (attrObj[key] === '') delete userObj[key]
|
|
467
|
-
else userObj[key] = attrObj[key]
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
357
|
+
userObj = scimgateway.patchObj(userObj, attrObj) // merge
|
|
472
358
|
|
|
473
359
|
if (!userObj.meta) {
|
|
474
360
|
const now = Date.now()
|
package/lib/scimgateway.ts
CHANGED
|
@@ -250,6 +250,9 @@ export class ScimGateway {
|
|
|
250
250
|
/** getEntitlements returns endpoint supported entitlements - e.g., plugin-entra-id returns available Entra tenant licenses as entitlements */
|
|
251
251
|
getEntitlements!: (baseEntity: string, getObj: Record<string, any>, attributes: Array<string>, ctx?: undefined | Record<string, any>) => any
|
|
252
252
|
|
|
253
|
+
/** getRoles returns endpoint supported roles - e.g., plugin-entra-id returns Entra permanent and eligible roles */
|
|
254
|
+
getRoles!: (baseEntity: string, getObj: Record<string, any>, attributes: Array<string>, ctx?: undefined | Record<string, any>) => any
|
|
255
|
+
|
|
253
256
|
/**
|
|
254
257
|
* postApi method is defined at the plugin and should handle incoming `"POST /api"` for creating an object and should be used according to your needs
|
|
255
258
|
* @param baseEntity used for multi tenant or multi endpoint support, either "undefined" or set by request url e.g., http://localhost:8880/loki2/Users gives baseEntity=loki2
|
|
@@ -488,12 +491,16 @@ export class ScimGateway {
|
|
|
488
491
|
description: 'Entitlement',
|
|
489
492
|
getMethod: 'getEntitlements',
|
|
490
493
|
}
|
|
494
|
+
handler.Roles = handler.roles = {
|
|
495
|
+
description: 'Role',
|
|
496
|
+
getMethod: 'getRoles',
|
|
497
|
+
}
|
|
491
498
|
handler.AppRoles = handler.approles = { // scim-stream
|
|
492
499
|
description: 'AppRole',
|
|
493
500
|
getMethod: 'getAppRoles',
|
|
494
501
|
}
|
|
495
502
|
/** handlers supported url paths */
|
|
496
|
-
const handlers = ['users', 'groups', 'bulk', 'entitlements', 'approles', 'api', 'schemas', 'resourcetypes', 'serviceproviderconfig', 'serviceproviderconfigs', 'oauth', '.well-known', 'logger']
|
|
503
|
+
const handlers = ['users', 'groups', 'bulk', 'entitlements', 'roles', 'approles', 'api', 'schemas', 'resourcetypes', 'serviceproviderconfig', 'serviceproviderconfigs', 'oauth', '.well-known', 'logger']
|
|
497
504
|
|
|
498
505
|
try {
|
|
499
506
|
if (!fs.existsSync(configDir + '/wsdls')) fs.mkdirSync(configDir + '/wsdls')
|
|
@@ -889,12 +896,45 @@ export class ScimGateway {
|
|
|
889
896
|
if (parts.length === 1) {
|
|
890
897
|
const org = scimResource.attributes.find((r: any) => r.name === item.mapTo)
|
|
891
898
|
let attr: any
|
|
892
|
-
if (org)
|
|
893
|
-
|
|
899
|
+
if (org) { // reusing original SCIM definition
|
|
900
|
+
attr = structuredClone(org)
|
|
901
|
+
if (item.subAttributes && Array.isArray(item.subAttributes) && attr?.subAttributes && Array.isArray(attr.subAttributes)) {
|
|
902
|
+
// any configuration subAttributes takes precidence
|
|
903
|
+
for (const el of item.subAttributes) {
|
|
904
|
+
if (typeof el !== 'object' || !el.name) continue
|
|
905
|
+
const existingSub = attr.subAttributes.find((sa: any) => sa.name === el.name)
|
|
906
|
+
if (existingSub) {
|
|
907
|
+
if (el.description) existingSub.description = el.description
|
|
908
|
+
if (el.mutability) existingSub.mutability = el.mutability
|
|
909
|
+
if (el.canonicalValues) existingSub.canonicalValues = el.canonicalValues
|
|
910
|
+
} else {
|
|
911
|
+
const newSub: Record<string, any> = {
|
|
912
|
+
name: el.name,
|
|
913
|
+
type: el.type ?? 'string',
|
|
914
|
+
multiValued: el.mulitvalue ?? false,
|
|
915
|
+
description: el.description ?? '',
|
|
916
|
+
required: false,
|
|
917
|
+
caseExact: false,
|
|
918
|
+
mutability: el.mutability ?? 'readWrite',
|
|
919
|
+
returned: 'default',
|
|
920
|
+
uniqueness: 'none',
|
|
921
|
+
}
|
|
922
|
+
if (el.canonicalValues) newSub.canonicalValues = el.canonicalValues
|
|
923
|
+
if (isV1) {
|
|
924
|
+
newSub.readOnly = newSub.mutability === 'readOnly'
|
|
925
|
+
delete newSub.mutability
|
|
926
|
+
delete newSub.returned
|
|
927
|
+
delete newSub.uniqueness
|
|
928
|
+
}
|
|
929
|
+
attr.subAttributes.push(newSub)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
894
934
|
attr = {
|
|
895
935
|
name: item.mapTo,
|
|
896
936
|
type: (item.type === 'boolean') ? item.type : 'string',
|
|
897
|
-
multiValued: item.type === 'array' ? true : false,
|
|
937
|
+
multiValued: (item.type === 'array' || item.type === 'complexArray') ? true : false,
|
|
898
938
|
description: item.description ?? '',
|
|
899
939
|
required: (item.mapTo === 'userName') ? true : false,
|
|
900
940
|
caseExact: false,
|
|
@@ -902,10 +942,33 @@ export class ScimGateway {
|
|
|
902
942
|
returned: 'default',
|
|
903
943
|
uniqueness: (item.mapTo === 'userName') ? 'server' : 'none',
|
|
904
944
|
}
|
|
905
|
-
if (item.type === 'complexObject') {
|
|
945
|
+
if (item.type === 'complexObject' || item.type === 'complexArray') {
|
|
906
946
|
attr.type = 'complex'
|
|
907
947
|
attr.multiValued = false
|
|
908
948
|
attr.subAttributes = []
|
|
949
|
+
if (item.subAttributes && Array.isArray(item.subAttributes)) {
|
|
950
|
+
for (const el of item.subAttributes) {
|
|
951
|
+
if (typeof el !== 'object' || el === null || !el.name) continue
|
|
952
|
+
const obj: Record<string, any> = {
|
|
953
|
+
name: el.name,
|
|
954
|
+
type: el.type ?? 'string',
|
|
955
|
+
multiValued: el.mulitvalue ?? false,
|
|
956
|
+
description: el.description ?? '',
|
|
957
|
+
required: false,
|
|
958
|
+
caseExact: false,
|
|
959
|
+
mutability: el.mutability ?? 'readWrite',
|
|
960
|
+
returned: 'default',
|
|
961
|
+
uniqueness: 'none',
|
|
962
|
+
}
|
|
963
|
+
if (isV1) {
|
|
964
|
+
obj.readOnly = false
|
|
965
|
+
delete obj.mutability
|
|
966
|
+
delete obj.returned
|
|
967
|
+
delete obj.uniqueness
|
|
968
|
+
}
|
|
969
|
+
attr.subAttributes.push(obj)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
909
972
|
}
|
|
910
973
|
}
|
|
911
974
|
if (item['x-agent-schema']) {
|
|
@@ -1343,10 +1406,10 @@ export class ScimGateway {
|
|
|
1343
1406
|
const baseEntity = ctx.routeObj.baseEntity
|
|
1344
1407
|
const id = decodeURIComponent(path.basename(ctx.routeObj.id ?? '', '.json')) // supports <id>.json
|
|
1345
1408
|
|
|
1346
|
-
if (!id || handle.getMethod === 'getEntitlements') {
|
|
1409
|
+
if (!id || handle.getMethod === 'getEntitlements' || handle.getMethod === 'getRoles') {
|
|
1347
1410
|
let err: Error
|
|
1348
1411
|
if (!id) err = new Error('missing id')
|
|
1349
|
-
else err = new Error(`GET /${handle.description}/${id} is not supported. Instead use filter query on users e.g., /users?filter=entitlements[value eq "xxx"]`)
|
|
1412
|
+
else err = new Error(`GET /${handle.description}/${id} is not supported. Instead use filter query on users e.g., /users?filter=entitlements[value eq "xxx"] or /users?filter=roles[value eq "xxx"]`)
|
|
1350
1413
|
const [e, statusCode] = utilsScim.jsonErr(this.config.scimgateway.scim.version, pluginName, 500, err)
|
|
1351
1414
|
ctx.response.status = statusCode
|
|
1352
1415
|
ctx.response.body = JSON.stringify(e)
|
|
@@ -1449,6 +1512,7 @@ export class ScimGateway {
|
|
|
1449
1512
|
// getUsers
|
|
1450
1513
|
// getGroups
|
|
1451
1514
|
// getEntitlements
|
|
1515
|
+
// getRoles
|
|
1452
1516
|
// ==========================================
|
|
1453
1517
|
const getHandler = async (ctx: Context) => {
|
|
1454
1518
|
const handle = handler[ctx.routeObj.handle]
|
|
@@ -1493,7 +1557,7 @@ export class ScimGateway {
|
|
|
1493
1557
|
}
|
|
1494
1558
|
|
|
1495
1559
|
let err
|
|
1496
|
-
if (handle.getMethod === 'getEntitlements') {
|
|
1560
|
+
if (handle.getMethod === 'getEntitlements' || handle.getMethod === 'getRoles') {
|
|
1497
1561
|
if (typeof (this as any)[handle.getMethod] !== 'function') err = new Error(`plugin method ${handle.getMethod}() not implemented`)
|
|
1498
1562
|
}
|
|
1499
1563
|
if (!err && getObj.attribute) {
|
|
@@ -1844,6 +1908,7 @@ export class ScimGateway {
|
|
|
1844
1908
|
logger.warn(`${gwName} ${handle.createMethod} succeeded, but corresponding ${handle.getMethod} ${obj?.value} failed with error: ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
1845
1909
|
}
|
|
1846
1910
|
if (res?.Resources && Array.isArray(res.Resources) && res.Resources.length === 1) {
|
|
1911
|
+
utils.extendObj(res.Resources[0], jsonBody) // we might have endpoint like Entra ID that hasn’t caught up yet due to internal sync - ensure returned object reflects changes by doing a merge with patch payload (convertedScim)
|
|
1847
1912
|
jsonBody = res.Resources[0]
|
|
1848
1913
|
}
|
|
1849
1914
|
delete jsonBody.password
|
|
@@ -2119,7 +2184,15 @@ export class ScimGateway {
|
|
|
2119
2184
|
const ob = { attribute: 'id', operator: 'eq', value: id }
|
|
2120
2185
|
logger.debug(`${gwName} calling ${handle.getMethod}`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
2121
2186
|
res = await (this as any)[handle.getMethod](baseEntity, ob, [], ctx.passThrough)
|
|
2187
|
+
|
|
2188
|
+
if (res?.Resources && Array.isArray(res.Resources) && res.Resources.length === 1) {
|
|
2189
|
+
// we might have endpoint like Entra ID that hasn’t caught up yet due to internal sync
|
|
2190
|
+
// ensure returned object reflects changes by doing a merge with patch payload (convertedScim)
|
|
2191
|
+
res.Resources[0] = this.patchObj(res.Resources[0], finalScimdata) // merge
|
|
2192
|
+
if (res.Resources[0].password) delete res.Resources[0].password
|
|
2193
|
+
}
|
|
2122
2194
|
}
|
|
2195
|
+
|
|
2123
2196
|
return response(res)
|
|
2124
2197
|
} catch (err: any) {
|
|
2125
2198
|
// check if error caused by: add existing member or remove none existing member => should not be an error
|
|
@@ -3430,6 +3503,7 @@ export class ScimGateway {
|
|
|
3430
3503
|
case 'GET users':
|
|
3431
3504
|
case 'GET groups':
|
|
3432
3505
|
case 'GET entitlements':
|
|
3506
|
+
case 'GET roles':
|
|
3433
3507
|
if (ctx.routeObj.id) await getHandlerId(ctx)
|
|
3434
3508
|
else await getHandler(ctx)
|
|
3435
3509
|
return await onAfterHandle(ctx)
|
|
@@ -3952,6 +4026,17 @@ export class ScimGateway {
|
|
|
3952
4026
|
return null
|
|
3953
4027
|
}
|
|
3954
4028
|
|
|
4029
|
+
/**
|
|
4030
|
+
* patchObj returns object updated with the modify user PATCH convertedScim which is sent to plugin
|
|
4031
|
+
* @param userObj "user object"
|
|
4032
|
+
* @param attrObj "the attrObj (convertedSCIM PATCH payload) sent to plugin"
|
|
4033
|
+
* @returns "user object updated according to PATCH payload - attrObj"
|
|
4034
|
+
**/
|
|
4035
|
+
patchObj(userObj: Record<string, any>, attrObj: Record<string, any>): any {
|
|
4036
|
+
const isMultiValueTypes = (attr: string) => this.isMultiValueTypes(attr)
|
|
4037
|
+
return utilsScim.patchObj(userObj, attrObj, isMultiValueTypes)
|
|
4038
|
+
}
|
|
4039
|
+
|
|
3955
4040
|
/**
|
|
3956
4041
|
* endpointMapper maps inbound SCIM and outbound endpoint attributes both ways
|
|
3957
4042
|
* @param direction 'outbound' (to the endpoint) or 'inbound' (SCIM response)
|
package/lib/utils-scim.ts
CHANGED
|
@@ -376,6 +376,126 @@ export function convertedScim20(obj: any, multiValueTypes: string[]): any {
|
|
|
376
376
|
return convertedScim(scimdata, multiValueTypes)
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* patchObj updates userObj with the PATCH convertedScim body sent to plugin
|
|
381
|
+
*/
|
|
382
|
+
export function patchObj(userObj: Record<string, any>, attrObj: Record<string, any>, isMultiValueTypes: (attribute: string) => boolean): any {
|
|
383
|
+
if (typeof userObj !== 'object') return userObj
|
|
384
|
+
if (typeof attrObj !== 'object') return userObj
|
|
385
|
+
|
|
386
|
+
for (const key in attrObj) {
|
|
387
|
+
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
|
|
388
|
+
const delArr = attrObj[key].filter(el => el.operation === 'delete')
|
|
389
|
+
const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
|
|
390
|
+
if (!userObj[key] || !Array.isArray(userObj[key])) userObj[key] = []
|
|
391
|
+
// delete
|
|
392
|
+
userObj[key] = userObj[key].filter((el: Record<string, any>) => {
|
|
393
|
+
const index = delArr.findIndex((e) => {
|
|
394
|
+
let elExist = false
|
|
395
|
+
for (const k in e) {
|
|
396
|
+
if (k === 'primary' || k === 'operation') continue
|
|
397
|
+
if (e[k] !== el[k]) {
|
|
398
|
+
elExist = false
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
elExist = true
|
|
402
|
+
}
|
|
403
|
+
return elExist
|
|
404
|
+
})
|
|
405
|
+
if (index >= 0) return false
|
|
406
|
+
else return true
|
|
407
|
+
})
|
|
408
|
+
// add
|
|
409
|
+
addArr.forEach((el) => {
|
|
410
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
411
|
+
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
412
|
+
const index = userObj[key].findIndex((e: Record<string, any>) => e.primary === el.primary)
|
|
413
|
+
if (index >= 0) {
|
|
414
|
+
if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
|
|
415
|
+
else userObj[key][index].primary = undefined // remove primary attribute, only one primary
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const index = userObj[key].findIndex((e: Record<string, any>, _index: number) => { // avoid adding existing
|
|
420
|
+
if (el.value && el.value === e.value && el.type === e.type) {
|
|
421
|
+
for (const k in el) {
|
|
422
|
+
e[k] = el[k]
|
|
423
|
+
}
|
|
424
|
+
return true
|
|
425
|
+
}
|
|
426
|
+
let elExist = false
|
|
427
|
+
for (const k in el) {
|
|
428
|
+
if (el[k] !== e[k]) {
|
|
429
|
+
if (k === 'primary') continue
|
|
430
|
+
elExist = false
|
|
431
|
+
break
|
|
432
|
+
}
|
|
433
|
+
elExist = true
|
|
434
|
+
}
|
|
435
|
+
return elExist
|
|
436
|
+
})
|
|
437
|
+
if (index < 0) userObj[key].push(el)
|
|
438
|
+
})
|
|
439
|
+
} else if (isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
440
|
+
if (!attrObj[key]) delete userObj[key] // blank or null
|
|
441
|
+
for (const el in attrObj[key]) {
|
|
442
|
+
attrObj[key][el].type = el
|
|
443
|
+
if (attrObj[key][el].operation && attrObj[key][el].operation === 'delete') { // delete multivalue
|
|
444
|
+
let type: any = el
|
|
445
|
+
if (type === 'undefined') type = undefined
|
|
446
|
+
if (Array.isArray(userObj[key])) {
|
|
447
|
+
userObj[key] = userObj[key].filter((e: Record<string, any>) => e.type !== type)
|
|
448
|
+
if (userObj[key].length < 1) delete userObj[key]
|
|
449
|
+
}
|
|
450
|
+
} else { // modify/create multivalue
|
|
451
|
+
if (!userObj[key]) userObj[key] = []
|
|
452
|
+
if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
|
|
453
|
+
const primVal = attrObj[key][el].primary
|
|
454
|
+
if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
|
|
455
|
+
const index = userObj[key].findIndex((e: Record<string, any>) => e.primary === primVal)
|
|
456
|
+
if (index >= 0) {
|
|
457
|
+
userObj[key][index].primary = undefined
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const found = userObj[key].find((e: Record<string, any>, i: any) => {
|
|
462
|
+
if (e.type === el || (!e.type && el === 'undefined')) {
|
|
463
|
+
for (const k in attrObj[key][el]) {
|
|
464
|
+
userObj[key][i][k] = attrObj[key][el][k]
|
|
465
|
+
if (k === 'type' && attrObj[key][el][k] === 'undefined') delete userObj[key][i][k] // don't store with type "undefined"
|
|
466
|
+
}
|
|
467
|
+
return true
|
|
468
|
+
} else return false
|
|
469
|
+
})
|
|
470
|
+
if (attrObj[key][el].type && attrObj[key][el].type === 'undefined') delete attrObj[key][el].type // don't store with type "undefined"
|
|
471
|
+
if (!found) userObj[key].push(attrObj[key][el]) // create
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
// None multi value attribute, blank will be deleted
|
|
476
|
+
if (typeof (attrObj[key]) === 'object' && attrObj[key] !== null) {
|
|
477
|
+
// name.familyName=Bianchi
|
|
478
|
+
if (!userObj[key]) userObj[key] = {} // e.g name object does not exist
|
|
479
|
+
for (const sub in attrObj[key]) {
|
|
480
|
+
if (!userObj[key]) userObj[key] = {}
|
|
481
|
+
if (Object.prototype.hasOwnProperty.call(attrObj[key][sub], 'value')
|
|
482
|
+
&& attrObj[key][sub].value === '') delete userObj[key][sub] // object having blank value attribute e.g. {"manager": {"value": "",...}}
|
|
483
|
+
else if (attrObj[key][sub] === '') delete userObj[key][sub]
|
|
484
|
+
else {
|
|
485
|
+
if (!userObj[key]) userObj[key] = {} // may have been deleted by length check below
|
|
486
|
+
userObj[key][sub] = attrObj[key][sub]
|
|
487
|
+
}
|
|
488
|
+
if (Object.keys(userObj[key]).length < 1) delete userObj[key]
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
if (attrObj[key] === '' || attrObj[key] === null) delete userObj[key]
|
|
492
|
+
else userObj[key] = attrObj[key]
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return userObj
|
|
497
|
+
}
|
|
498
|
+
|
|
379
499
|
// recursiveStrMap is used by endpointMapper() for converting obj according to endpointMap type definition
|
|
380
500
|
const recursiveStrMap = function (direction: string, dotMap: any, obj: any, dotPath: any) {
|
|
381
501
|
for (const key in obj) {
|
package/package.json
CHANGED