scimgateway 5.3.4 → 5.3.6

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.
Files changed (50) hide show
  1. package/.gitattribute +2 -0
  2. package/.github/dependabot.yml +0 -0
  3. package/.github/workflows/test-master.yml +0 -0
  4. package/.github/workflows/test-release.yml +0 -0
  5. package/.travis.yml +0 -0
  6. package/LICENSE +0 -0
  7. package/README.md +13 -0
  8. package/bun.lock +57 -171
  9. package/config/docker/DataDockerfile +0 -0
  10. package/config/docker/Dockerfile +0 -0
  11. package/config/docker/dbinit/init.sql +0 -0
  12. package/config/docker/docker-compose-debug.yml +0 -0
  13. package/config/docker/docker-compose-mssql.yml +0 -0
  14. package/config/docker/docker-compose.yml +0 -0
  15. package/config/plugin-entra-id.json +0 -0
  16. package/config/plugin-ldap.json +0 -0
  17. package/config/plugin-mongodb.json +0 -0
  18. package/config/plugin-mssql.json +0 -0
  19. package/config/plugin-saphana.json +0 -0
  20. package/config/plugin-soap.json +0 -0
  21. package/config/resources/Azure - ScimGateway.xml +0 -0
  22. package/config/wsdls/GroupService.wsdl +0 -0
  23. package/config/wsdls/UserService.wsdl +0 -0
  24. package/eslint.config.js +0 -0
  25. package/lib/countries.json +0 -0
  26. package/lib/helper-rest.ts +0 -0
  27. package/lib/logger.ts +0 -0
  28. package/lib/plugin-api.ts +0 -0
  29. package/lib/plugin-entra-id.ts +0 -0
  30. package/lib/plugin-ldap.ts +0 -0
  31. package/lib/plugin-loki.ts +0 -0
  32. package/lib/plugin-mongodb.ts +0 -0
  33. package/lib/plugin-mssql.ts +0 -0
  34. package/lib/plugin-saphana.ts +0 -0
  35. package/lib/plugin-scim.ts +0 -0
  36. package/lib/plugin-soap.ts +0 -0
  37. package/lib/postinstall.ts +0 -0
  38. package/lib/samlAssertion.ts +0 -0
  39. package/lib/scim-stream.js +0 -0
  40. package/lib/scimdef-v1.json +1 -1
  41. package/lib/scimdef-v2.json +1 -1
  42. package/lib/scimgateway.ts +115 -33
  43. package/lib/utils-scim.ts +1 -0
  44. package/lib/utils.ts +27 -2
  45. package/package.json +10 -10
  46. package/test/lib/plugin-api_test.ts +0 -0
  47. package/test/lib/plugin-loki_test.ts +53 -8
  48. package/test/lib/plugin-mongodb_test.ts_excluded +0 -0
  49. package/test/lib/plugin-scim_test.ts +8 -6
  50. package/tsconfig.json +0 -0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/eslint.config.js CHANGED
File without changes
File without changes
File without changes
package/lib/logger.ts CHANGED
File without changes
package/lib/plugin-api.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "changePassword": { "supported": true },
16
16
  "sort": { "supported": false },
17
- "etag": { "supported": false },
17
+ "etag": { "supported": true },
18
18
  "authenticationSchemes": [
19
19
  {
20
20
  "type": "httpbasic",
@@ -20,7 +20,7 @@
20
20
  "supported": false
21
21
  },
22
22
  "etag": {
23
- "supported": false
23
+ "supported": true
24
24
  },
25
25
  "documentationUri": "https://elshaug.xyz",
26
26
  "authenticationSchemes": [
@@ -583,8 +583,11 @@ export class ScimGateway {
583
583
  }
584
584
 
585
585
  if (ctx.response.status && (ctx.response.status < 200 || ctx.response.status > 299)) {
586
- if (ctx.response.status === 404) logger.warn(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${userName} ${ctx.response.status} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${outbound}`)
587
- else logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${userName} ${ctx.response.status} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${outbound}`)
586
+ if (ctx.response.status === 412 || ctx.response.status === 304) {
587
+ logger.info(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${userName} ${ctx.response.status} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${outbound}`)
588
+ } else if (ctx.response.status === 404) {
589
+ logger.warn(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${userName} ${ctx.response.status} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${outbound}`)
590
+ } else logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${userName} ${ctx.response.status} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${outbound}`)
588
591
  } else logger.info(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${ctx.response.status} ${userName} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${outbound}`)
589
592
  }
590
593
 
@@ -1048,8 +1051,8 @@ export class ScimGateway {
1048
1051
  ctx.response.body = JSON.stringify(e)
1049
1052
  return
1050
1053
  }
1051
- if (ctx.query.attributes) ctx.query.attributes = ctx.query.attributes.split(',').map((item: string) => item.trim()).join()
1052
- if (ctx.query.excludedAttributes) ctx.query.excludedAttributes = ctx.query.excludedAttributes.split(',').map((item: string) => item.trim()).join()
1054
+ if (ctx.query.attributes) ctx.query.attributes = ctx.query.attributes.split(',').filter(Boolean).map((item: string) => item.trim()).join()
1055
+ if (ctx.query.excludedAttributes) ctx.query.excludedAttributes = ctx.query.excludedAttributes.split(',').filter(Boolean).map((item: string) => item.trim()).join()
1053
1056
 
1054
1057
  const getObj = {
1055
1058
  attribute: 'id',
@@ -1063,7 +1066,7 @@ export class ScimGateway {
1063
1066
  const ob = utils.copyObj(getObj)
1064
1067
  const attributes = ctx.query.attributes ? ctx.query.attributes.split(',').map((item: string) => item.trim()) : []
1065
1068
  logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`)
1066
- let res = await (this as any)[handle.getMethod](baseEntity, ob, attributes, ctx.passThrough)
1069
+ let res = await (this as any)[handle.getMethod](baseEntity, ob, [], ctx.passThrough)
1067
1070
 
1068
1071
  let scimdata: { [key: string]: any } = {
1069
1072
  Resources: [],
@@ -1085,13 +1088,31 @@ export class ScimGateway {
1085
1088
  ctx.response.body = JSON.stringify(e)
1086
1089
  return
1087
1090
  }
1088
- let userObj = scimdata.Resources[0]
1091
+ const userObj = scimdata.Resources[0]
1092
+ const eTag = utils.getEtag(userObj)
1093
+
1094
+ const eTagIfMatch = ctx.request.headers.get('if-match')?.split(',').map((item: string) => item.trim()).filter(Boolean)
1095
+ const eTagIfNoneMatch = ctx.request.headers.get('if-none-match')?.split(',').map((item: string) => item.trim()).filter(Boolean)
1096
+
1097
+ if (eTag) {
1098
+ if (eTagIfMatch && !eTagIfMatch.includes(eTag) && !eTagIfMatch.includes('*')) {
1099
+ ctx.response.headers.set('ETag', eTag)
1100
+ ctx.response.status = 412 // Precondition Failed
1101
+ const err = new Error(`ETag If-Match mismatch: ${eTagIfMatch} != ${eTag}`)
1102
+ const [e] = utilsScim.jsonErr(this.config.scimgateway.scim.version, pluginName, ctx.response.status, err)
1103
+ ctx.response.body = JSON.stringify(e)
1104
+ return
1105
+ } else if (eTagIfNoneMatch && (eTagIfNoneMatch.includes(eTag) || eTagIfNoneMatch.includes('*'))) {
1106
+ ctx.response.headers.set('ETag', eTag)
1107
+ ctx.response.status = 304 // Not Modified
1108
+ ctx.response.body = ''
1109
+ return
1110
+ }
1111
+ }
1089
1112
 
1090
1113
  // check for user attribute groups and include if needed
1091
1114
  if (handle.getMethod === handler.users.getMethod && Object.keys(userObj).length > 0) {
1092
- let arrAttr: string[] = []
1093
- if (ctx.query.attributes) arrAttr = ctx.query.attributes.split(',')
1094
- if ((!ctx.query.attributes || arrAttr.includes('groups'))) { // include groups
1115
+ if (attributes.length === 0 || attributes.includes('groups')) { // include groups
1095
1116
  if (!userObj.groups && userObj.id) {
1096
1117
  userObj.groups = await getMemberOf(baseEntity, userObj.id, handler.groups.getMethod, ctx.passThrough)
1097
1118
  }
@@ -1110,6 +1131,8 @@ export class ScimGateway {
1110
1131
  }
1111
1132
  }
1112
1133
  ctx.response.body = JSON.stringify(scimdata)
1134
+ if (eTag) ctx.response.headers.set('ETag', eTag)
1135
+ if (scimdata?.meta?.location) ctx.response.headers.set('Location', scimdata.meta.location)
1113
1136
  } catch (err: any) {
1114
1137
  ctx.response.status = 404
1115
1138
  const [e, customErrorCode] = utilsScim.jsonErr(this.config.scimgateway.scim.version, pluginName, ctx.response.status, err)
@@ -1126,8 +1149,8 @@ export class ScimGateway {
1126
1149
  const getHandler = async (ctx: Context) => {
1127
1150
  const handle = handler[ctx.routeObj.handle]
1128
1151
  const baseEntity = ctx.routeObj.baseEntity
1129
- if (ctx.query.attributes) ctx.query.attributes = ctx.query.attributes.split(',').map((item: string) => item.trim()).join()
1130
- if (ctx.query.excludedAttributes) ctx.query.excludedAttributes = ctx.query.excludedAttributes.split(',').map((item: string) => item.trim()).join()
1152
+ if (ctx.query.attributes) ctx.query.attributes = ctx.query.attributes.split(',').filter(Boolean).map((item: string) => item.trim()).join()
1153
+ if (ctx.query.excludedAttributes) ctx.query.excludedAttributes = ctx.query.excludedAttributes.split(',').filter(Boolean).map((item: string) => item.trim()).join()
1131
1154
 
1132
1155
  const getObj: any = {
1133
1156
  attribute: undefined,
@@ -1280,7 +1303,7 @@ export class ScimGateway {
1280
1303
  }
1281
1304
  if (getObjArr.length > 0) {
1282
1305
  const getObj = async (o: Record<string, any>) => {
1283
- return await (this as any)[handle.getMethod](baseEntity, o, attributes, ctx.passThrough)
1306
+ return await (this as any)[handle.getMethod](baseEntity, o, [], ctx.passThrough)
1284
1307
  }
1285
1308
  const chunk = 5
1286
1309
  const chunkRes: Record<string, any>[] = []
@@ -1304,14 +1327,12 @@ export class ScimGateway {
1304
1327
 
1305
1328
  if (!res) { // standard
1306
1329
  logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`)
1307
- res = await (this as any)[handle.getMethod](baseEntity, obj, attributes, ctx.passThrough)
1330
+ res = await (this as any)[handle.getMethod](baseEntity, obj, [], ctx.passThrough)
1308
1331
  }
1309
1332
  // check for user attribute groups and include if needed
1310
1333
  if (Array.isArray(res?.Resources)) {
1311
1334
  if (handle.getMethod === handler.users.getMethod) {
1312
- let arrAttr: string[] = []
1313
- if (ctx.query.attributes) arrAttr = ctx.query.attributes.split(',')
1314
- if ((!ctx.query.attributes || arrAttr.includes('groups'))) { // include groups
1335
+ if (attributes.length === 0 || attributes.includes('groups')) { // include groups
1315
1336
  for (let i = 0; i < res.Resources.length; i++) {
1316
1337
  const userObj = res.Resources[i]
1317
1338
  if (!userObj.id) break
@@ -1335,8 +1356,9 @@ export class ScimGateway {
1335
1356
 
1336
1357
  let location: string | undefined = ctx.origin + ctx.path
1337
1358
  if (this.config.scimgateway.scim.skipMetaLocation) location = undefined
1338
- else if (ctx.query.attributes || (ctx.query.excludedAttributes && ctx.query.excludedAttributes.includes('meta'))) location = undefined
1359
+ else if (ctx.query.excludedAttributes && ctx.query.excludedAttributes.includes('meta')) location = undefined
1339
1360
  for (let i = 0; i < scimdata.Resources.length; i++) {
1361
+ utils.getEtag(scimdata.Resources[i])
1340
1362
  scimdata.Resources[i] = utils.stripObj(scimdata.Resources[i], ctx.query.attributes, ctx.query.excludedAttributes)
1341
1363
  }
1342
1364
  scimdata = utilsScim.addResources(scimdata, ctx.query.startIndex, ctx.query.sortBy, ctx.query.sortOrder)
@@ -1450,6 +1472,7 @@ export class ScimGateway {
1450
1472
  if (obj && obj.id) jsonBody = obj // id found, using returned object
1451
1473
  }
1452
1474
 
1475
+ const eTag = utils.getEtag(jsonBody)
1453
1476
  if (addGrps.length > 0 && handle.createMethod === 'createUser') { // add group membership
1454
1477
  const addGroups = async (groupId: string) => {
1455
1478
  return await (this as any)[handler.groups.modifyMethod](baseEntity, groupId, { members: [{ value: jsonBody.id }] }, ctx.passThrough)
@@ -1470,12 +1493,10 @@ export class ScimGateway {
1470
1493
  const location = ctx.origin + `${ctx.path}/${encodeURIComponent(decodeURIComponent(jsonBody.id))}`
1471
1494
  if (!jsonBody.meta) jsonBody.meta = {}
1472
1495
  jsonBody.meta.location = location
1473
- const response = ctx.response as any
1474
- if (!response.headers) response.headers = {}
1475
- response.headers.Location = location
1476
1496
  }
1477
- delete jsonBody.password
1478
1497
  jsonBody = utilsScim.addSchemas(jsonBody, isScimv2, handle.description, undefined)
1498
+ if (eTag) ctx.response.headers.set('ETag', eTag)
1499
+ if (jsonBody?.meta?.location) ctx.response.headers.set('Location', jsonBody.meta.location)
1479
1500
  ctx.response.status = 201
1480
1501
  ctx.response.body = JSON.stringify(jsonBody)
1481
1502
  } catch (err: any) {
@@ -1547,8 +1568,8 @@ export class ScimGateway {
1547
1568
  // example: {"members":[{"value":"bjensen"}],"schemas":["urn:scim:schemas:core:1.0"]}
1548
1569
  //
1549
1570
  const patchHandler = async (ctx: Context) => {
1550
- if (ctx.query.attributes) ctx.query.attributes = ctx.query.attributes.split(',').map((item: string) => item.trim()).join()
1551
- if (ctx.query.excludedAttributes) ctx.query.excludedAttributes = ctx.query.excludedAttributes.split(',').map((item: any) => item.trim()).join()
1571
+ if (ctx.query.attributes) ctx.query.attributes = ctx.query.attributes.split(',').filter(Boolean).map((item: string) => item.trim()).join()
1572
+ if (ctx.query.excludedAttributes) ctx.query.excludedAttributes = ctx.query.excludedAttributes.split(',').filter(Boolean).map((item: any) => item.trim()).join()
1552
1573
  const handle = handler[ctx.routeObj.handle]
1553
1574
  const baseEntity = ctx.routeObj.baseEntity
1554
1575
  const id = ctx.routeObj.id ? decodeURIComponent(ctx.routeObj.id) : ctx.routeObj.id
@@ -1567,6 +1588,43 @@ export class ScimGateway {
1567
1588
  }
1568
1589
 
1569
1590
  logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Modify ${handle.description}] id=${id}`)
1591
+
1592
+ const eTagIfMatch = ctx.request.headers.get('if-match')?.split(',').map((item: string) => item.trim()).filter(Boolean)
1593
+ const eTagIfNoneMatch = ctx.request.headers.get('if-none-match')?.split(',').map((item: string) => item.trim()).filter(Boolean)
1594
+ if (eTagIfMatch || eTagIfNoneMatch) {
1595
+ let eTag = ''
1596
+ if (handle.getMethod === handler.users.getMethod || handle.getMethod === handler.groups.getMethod) { // getUsers or getGroups implemented
1597
+ const ob = { attribute: 'id', operator: 'eq', value: id }
1598
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`)
1599
+ const res = await (this as any)[handle.getMethod](baseEntity, ob, [], ctx.passThrough)
1600
+ if (res) {
1601
+ if (res.Resources && Array.isArray(res.Resources)) {
1602
+ if (res.Resources.length === 1) {
1603
+ eTag = utils.getEtag(res.Resources[0])
1604
+ }
1605
+ } else if (Array.isArray(res)) {
1606
+ if (res.length === 1) {
1607
+ eTag = utils.getEtag(res[0])
1608
+ }
1609
+ } else if (typeof (res) === 'object' && res !== null) eTag = utils.getEtag(res[0])
1610
+ }
1611
+ }
1612
+ if (eTag)
1613
+ if (eTagIfMatch && !eTagIfMatch.includes(eTag) && !eTagIfMatch.includes('*')) {
1614
+ ctx.response.headers.set('ETag', eTag)
1615
+ ctx.response.status = 412 // Precondition Failed
1616
+ const err = new Error(`ETag If-Match mismatch: ${eTagIfMatch} != ${eTag}`)
1617
+ const [e] = utilsScim.jsonErr(this.config.scimgateway.scim.version, pluginName, ctx.response.status, err)
1618
+ ctx.response.body = JSON.stringify(e)
1619
+ return
1620
+ } else if (eTagIfNoneMatch && (eTagIfNoneMatch.includes(eTag) || eTagIfNoneMatch.includes('*'))) {
1621
+ ctx.response.headers.set('ETag', eTag)
1622
+ ctx.response.status = 412 // Precondition Failed
1623
+ ctx.response.body = ''
1624
+ return
1625
+ }
1626
+ }
1627
+
1570
1628
  let scimdata: any, err: any
1571
1629
  if (jsonBody.Operations) [scimdata, err] = utilsScim.convertedScim20(jsonBody, this.multiValueTypes) // v2.0
1572
1630
  else [scimdata, err] = utilsScim.convertedScim(jsonBody, this.multiValueTypes) // v1.1
@@ -1594,7 +1652,7 @@ export class ScimGateway {
1594
1652
  try {
1595
1653
  let res: any
1596
1654
  if (Array.isArray(scimdata.members) && scimdata.members.length === 0 && handle.modifyMethod === 'modifyGroup') {
1597
- res = await replaceUsrGrp(ctx.routeObj.handle, baseEntity, id, scimdata, this.config.scimgateway.scim.usePutSoftSync, ctx.passThrough)
1655
+ res = await replaceUsrGrp(ctx.routeObj.handle, baseEntity, id, scimdata, this.config.scimgateway.scim.usePutSoftSync, ctx.passThrough, undefined)
1598
1656
  } else {
1599
1657
  logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.modifyMethod} and awaiting result`)
1600
1658
  res = await (this as any)[handle.modifyMethod](baseEntity, id, scimdata, ctx.passThrough)
@@ -1621,9 +1679,8 @@ export class ScimGateway {
1621
1679
  return
1622
1680
  }
1623
1681
  const ob = { attribute: 'id', operator: 'eq', value: id }
1624
- const attributes = ctx.query.attributes ? ctx.query.attributes.split(',').map((item: string) => item.trim()) : []
1625
1682
  logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`)
1626
- res = await (this as any)[handle.getMethod](baseEntity, ob, attributes, ctx.passThrough)
1683
+ res = await (this as any)[handle.getMethod](baseEntity, ob, [], ctx.passThrough)
1627
1684
  }
1628
1685
 
1629
1686
  scimdata = {
@@ -1642,16 +1699,17 @@ export class ScimGateway {
1642
1699
  }
1643
1700
 
1644
1701
  const userObj = scimdata.Resources[0]
1702
+ const eTag = utils.getEtag(userObj)
1645
1703
  if (!this.config.scimgateway.scim.skipMetaLocation) {
1646
1704
  const location = ctx.origin + ctx.path
1647
1705
  if (!userObj.meta) userObj.meta = {}
1648
1706
  userObj.meta.location = location
1649
- const response = ctx.response as any
1650
- if (!response.headers) response.headers = {}
1651
- response.headers.Location = location
1652
1707
  }
1708
+
1653
1709
  scimdata = utils.stripObj(userObj, ctx.query.attributes, ctx.query.excludedAttributes)
1654
1710
  scimdata = utilsScim.addSchemas(scimdata, isScimv2, handle.description, undefined)
1711
+ if (eTag) ctx.response.headers.set('ETag', eTag)
1712
+ if (scimdata?.meta?.location) ctx.response.headers.set('Location', scimdata.meta.location)
1655
1713
  ctx.response.status = 200
1656
1714
  ctx.response.body = JSON.stringify(scimdata)
1657
1715
  } catch (err: any) {
@@ -1667,7 +1725,7 @@ export class ScimGateway {
1667
1725
  // Replace User
1668
1726
  // Replace Group
1669
1727
  // ==========================================
1670
- const replaceUsrGrp = async (h: string, baseEntity: string, id: string | undefined, obj: Record<string, any>, usePutSoftSync: boolean | undefined, ctxPassThrough: Record<string, any> | undefined) => {
1728
+ const replaceUsrGrp = async (h: string, baseEntity: string, id: string | undefined, obj: Record<string, any>, usePutSoftSync: boolean | undefined, ctxPassThrough: Record<string, any> | undefined, headers: Headers | undefined) => {
1671
1729
  const handle = handler[h] // h = Users/Groups
1672
1730
  if (!id) throw new Error('missing id')
1673
1731
  id = decodeURIComponent(id)
@@ -1690,6 +1748,23 @@ export class ScimGateway {
1690
1748
  throw err
1691
1749
  }
1692
1750
 
1751
+ const eTagIfMatch = headers ? headers.get('if-match')?.split(',').map((item: string) => item.trim()).filter(Boolean) : undefined
1752
+ const eTagIfNoneMatch = headers ? headers.get('if-none-match')?.split(',').map((item: string) => item.trim()).filter(Boolean) : undefined
1753
+ if (eTagIfMatch || eTagIfNoneMatch) {
1754
+ const eTag = utils.getEtag(currentObj)
1755
+ if (eTag) {
1756
+ if (eTagIfMatch && !eTagIfMatch.includes(eTag) && !eTagIfMatch.includes('*')) {
1757
+ const err = new Error(`put using method ${handle.getMethod} error: ETag If-Match mismatch: ${eTagIfMatch} != ${eTag}`)
1758
+ err.name += '#412' // Precondition Failed
1759
+ throw err
1760
+ } else if (eTagIfNoneMatch && (eTagIfNoneMatch.includes(eTag) || eTagIfNoneMatch.includes('*'))) {
1761
+ const err = new Error(`put using method ${handle.getMethod} error: ETag If-None-Match mismatch: ${eTagIfNoneMatch} = ${eTag}`)
1762
+ err.name += '#412' // Precondition Failed
1763
+ throw err
1764
+ }
1765
+ }
1766
+ }
1767
+
1693
1768
  const activeExists = Object.prototype.hasOwnProperty.call(obj, 'active')
1694
1769
  let objGroups: any
1695
1770
  if (obj.groups) {
@@ -1817,7 +1892,10 @@ export class ScimGateway {
1817
1892
  try {
1818
1893
  if (!obj) throw new Error('missing body')
1819
1894
  if (typeof obj !== 'object') throw new Error('body is not JSON')
1820
- await replaceUsrGrp(handle, baseEntity, id, obj, this.config.scimgateway.scim.usePutSoftSync, ctx.passThrough)
1895
+ const headers = ctx.request.headers
1896
+ await replaceUsrGrp(handle, baseEntity, id, obj, this.config.scimgateway.scim.usePutSoftSync, ctx.passThrough, headers)
1897
+ ctx.request.headers.delete('if-match')
1898
+ ctx.request.headers.delete('if-none-match')
1821
1899
  await getHandlerId(ctx) // ctx.response.body now updated with userObject to be returned
1822
1900
  if (ctx.response.status && ctx.response.status !== 200) { // clear any get error
1823
1901
  ctx.response.body = undefined
@@ -1844,6 +1922,7 @@ export class ScimGateway {
1844
1922
  method: string
1845
1923
  path: string
1846
1924
  bulkId?: string
1925
+ version?: string
1847
1926
  data?: any
1848
1927
  }
1849
1928
 
@@ -1906,6 +1985,8 @@ export class ScimGateway {
1906
1985
  signal: ctx.request.signal,
1907
1986
  body: JSON.stringify(resolvedData),
1908
1987
  }) as Request & { raw: IncomingMessage }
1988
+ if (op.version) bulkReq.headers.set('if-match', op.version)
1989
+
1909
1990
  const bulkCtx = await onBeforeHandle(bulkReq, ctx.ip)
1910
1991
 
1911
1992
  if (!resolvedErr) {
@@ -2679,9 +2760,10 @@ export class ScimGateway {
2679
2760
  idleTimeout,
2680
2761
  hostname, // hostname === 'localhost' ? hostname : undefined, // bun defaults to '0.0.0.0', but using '0.0.0.0.' or other ip like '127.0.0.1' becomes extremly slow - bun bug
2681
2762
  tls,
2682
- async fetch(req: Request & { raw: IncomingMessage }, srv) {
2763
+ async fetch(req, srv) {
2683
2764
  // start route processing and return response
2684
- return await route(req, srv.requestIP(req)?.address || '')
2765
+ const reqWithRaw = req as Request & { raw: IncomingMessage }
2766
+ return await route(reqWithRaw, srv.requestIP(req)?.address || '')
2685
2767
  },
2686
2768
  error(err) {
2687
2769
  logger.error(`${gwName} internal error: ${err.message}`)
package/lib/utils-scim.ts CHANGED
@@ -8,6 +8,7 @@ type SCIMBulkOperation = {
8
8
  method: string
9
9
  path: string
10
10
  bulkId?: string
11
+ version?: string
11
12
  data?: any
12
13
  }
13
14
 
package/lib/utils.ts CHANGED
@@ -433,7 +433,9 @@ export const stripObj = (obj: Record<string, any>, attributes?: string, excluded
433
433
  let arrRet = []
434
434
  const arrCheckEmpty: any = []
435
435
  if (attributes) {
436
- const arrAttr = attributes.split(',').map(item => item.trim())
436
+ const arrAttr = attributes.split(',').filter(Boolean).map(item => item.trim())
437
+ if (!arrAttr.includes('id')) arrAttr.push('id') // always include id
438
+ if (!arrAttr.includes('meta')) arrAttr.push('meta') // include meta if supported by endpoint
437
439
  arrRet = arrObj.map((obj) => {
438
440
  const ret: Record<string, any> = {}
439
441
  for (let i = 0; i < arrAttr.length; i++) {
@@ -473,7 +475,7 @@ export const stripObj = (obj: Record<string, any>, attributes?: string, excluded
473
475
  return ret
474
476
  })
475
477
  } else if (excludedAttributes) {
476
- const arrAttr = excludedAttributes.split(',').map(item => item.trim())
478
+ const arrAttr = excludedAttributes.split(',').filter(Boolean).map(item => item.trim()).filter(item => item !== 'id' && item !== 'meta')
477
479
  arrRet = arrObj.map((obj) => {
478
480
  const ret: any = copyObj(obj)
479
481
  for (let i = 0; i < arrAttr.length; i++) {
@@ -684,3 +686,26 @@ export const getBase64CertificateThumbprint = function (pemCertContent: string,
684
686
 
685
687
  return base64Url
686
688
  }
689
+
690
+ /**
691
+ * getEtag returns an ETag for the given object and updates the object with the ETag in meta.version
692
+ * @param obj full object to calculate ETag from
693
+ * @returns ETag string as W/"<hash>"
694
+ */
695
+ export const getEtag = function (obj: Record<string, any>): string {
696
+ if (typeof obj !== 'object' || obj === null) return ''
697
+ const hash = crypto
698
+ .createHash('md5')
699
+ .update(JSON.stringify(obj), 'utf8')
700
+ .digest('base64')
701
+ .substring(0, 22)
702
+
703
+ let eTag = ''
704
+ if (obj?.meta?.version) eTag = obj.meta.version
705
+ else {
706
+ eTag = `W/"${hash}"`
707
+ if (!obj.meta) obj.meta = {}
708
+ obj.meta.version = eTag
709
+ }
710
+ return eTag
711
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.3.4",
3
+ "version": "5.3.6",
4
4
  "type": "module",
5
5
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
6
6
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
@@ -51,25 +51,25 @@
51
51
  "lokijs": "^1.5.12",
52
52
  "mongodb": "^6.16.0",
53
53
  "node-machine-id": "1.1.12",
54
- "nodemailer": "^7.0.0",
54
+ "nodemailer": "^7.0.3",
55
55
  "passport": "^0.7.0",
56
56
  "passport-azure-ad": "^4.3.5",
57
57
  "saml": "^3.0.1"
58
58
  },
59
59
  "peerDependencies": {
60
- "typescript": "^5.0.0"
60
+ "typescript": "^5.6.3"
61
61
  },
62
62
  "devDependencies": {
63
- "@stylistic/eslint-plugin": "^4.2.0",
63
+ "@stylistic/eslint-plugin": "^4.4.0",
64
64
  "@types/bun": "latest",
65
65
  "@types/dot-object": "^2.1.6",
66
- "@types/jsonwebtoken": "^9.0.6",
66
+ "@types/jsonwebtoken": "^9.0.9",
67
67
  "@types/node": "latest",
68
- "@types/nodemailer": "^6.4.14",
69
- "@types/passport": "^1.0.16",
68
+ "@types/nodemailer": "^6.4.17",
69
+ "@types/passport": "^1.0.17",
70
70
  "@types/passport-azure-ad": "^4.3.6",
71
- "@typescript-eslint/eslint-plugin": "^8.31.1",
72
- "@typescript-eslint/parser": "^8.31.1",
73
- "eslint": "^9.26.0"
71
+ "@typescript-eslint/eslint-plugin": "^8.33.0",
72
+ "@typescript-eslint/parser": "^8.33.0",
73
+ "eslint": "^9.27.0"
74
74
  }
75
75
  }
File without changes