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.
- package/.gitattribute +2 -0
- package/.github/dependabot.yml +0 -0
- package/.github/workflows/test-master.yml +0 -0
- package/.github/workflows/test-release.yml +0 -0
- package/.travis.yml +0 -0
- package/LICENSE +0 -0
- package/README.md +13 -0
- package/bun.lock +57 -171
- package/config/docker/DataDockerfile +0 -0
- package/config/docker/Dockerfile +0 -0
- package/config/docker/dbinit/init.sql +0 -0
- package/config/docker/docker-compose-debug.yml +0 -0
- package/config/docker/docker-compose-mssql.yml +0 -0
- package/config/docker/docker-compose.yml +0 -0
- package/config/plugin-entra-id.json +0 -0
- package/config/plugin-ldap.json +0 -0
- package/config/plugin-mongodb.json +0 -0
- package/config/plugin-mssql.json +0 -0
- package/config/plugin-saphana.json +0 -0
- package/config/plugin-soap.json +0 -0
- package/config/resources/Azure - ScimGateway.xml +0 -0
- package/config/wsdls/GroupService.wsdl +0 -0
- package/config/wsdls/UserService.wsdl +0 -0
- package/eslint.config.js +0 -0
- package/lib/countries.json +0 -0
- package/lib/helper-rest.ts +0 -0
- package/lib/logger.ts +0 -0
- package/lib/plugin-api.ts +0 -0
- package/lib/plugin-entra-id.ts +0 -0
- package/lib/plugin-ldap.ts +0 -0
- package/lib/plugin-loki.ts +0 -0
- package/lib/plugin-mongodb.ts +0 -0
- package/lib/plugin-mssql.ts +0 -0
- package/lib/plugin-saphana.ts +0 -0
- package/lib/plugin-scim.ts +0 -0
- package/lib/plugin-soap.ts +0 -0
- package/lib/postinstall.ts +0 -0
- package/lib/samlAssertion.ts +0 -0
- package/lib/scim-stream.js +0 -0
- package/lib/scimdef-v1.json +1 -1
- package/lib/scimdef-v2.json +1 -1
- package/lib/scimgateway.ts +115 -33
- package/lib/utils-scim.ts +1 -0
- package/lib/utils.ts +27 -2
- package/package.json +10 -10
- package/test/lib/plugin-api_test.ts +0 -0
- package/test/lib/plugin-loki_test.ts +53 -8
- package/test/lib/plugin-mongodb_test.ts_excluded +0 -0
- package/test/lib/plugin-scim_test.ts +8 -6
- package/tsconfig.json +0 -0
|
File without changes
|
package/config/docker/Dockerfile
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/config/plugin-ldap.json
CHANGED
|
File without changes
|
|
File without changes
|
package/config/plugin-mssql.json
CHANGED
|
File without changes
|
|
File without changes
|
package/config/plugin-soap.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/eslint.config.js
CHANGED
|
File without changes
|
package/lib/countries.json
CHANGED
|
File without changes
|
package/lib/helper-rest.ts
CHANGED
|
File without changes
|
package/lib/logger.ts
CHANGED
|
File without changes
|
package/lib/plugin-api.ts
CHANGED
|
File without changes
|
package/lib/plugin-entra-id.ts
CHANGED
|
File without changes
|
package/lib/plugin-ldap.ts
CHANGED
|
File without changes
|
package/lib/plugin-loki.ts
CHANGED
|
File without changes
|
package/lib/plugin-mongodb.ts
CHANGED
|
File without changes
|
package/lib/plugin-mssql.ts
CHANGED
|
File without changes
|
package/lib/plugin-saphana.ts
CHANGED
|
File without changes
|
package/lib/plugin-scim.ts
CHANGED
|
File without changes
|
package/lib/plugin-soap.ts
CHANGED
|
File without changes
|
package/lib/postinstall.ts
CHANGED
|
File without changes
|
package/lib/samlAssertion.ts
CHANGED
|
File without changes
|
package/lib/scim-stream.js
CHANGED
|
File without changes
|
package/lib/scimdef-v1.json
CHANGED
package/lib/scimdef-v2.json
CHANGED
package/lib/scimgateway.ts
CHANGED
|
@@ -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 ===
|
|
587
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
|
2763
|
+
async fetch(req, srv) {
|
|
2683
2764
|
// start route processing and return response
|
|
2684
|
-
|
|
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
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.
|
|
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.
|
|
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.
|
|
60
|
+
"typescript": "^5.6.3"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@stylistic/eslint-plugin": "^4.
|
|
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.
|
|
66
|
+
"@types/jsonwebtoken": "^9.0.9",
|
|
67
67
|
"@types/node": "latest",
|
|
68
|
-
"@types/nodemailer": "^6.4.
|
|
69
|
-
"@types/passport": "^1.0.
|
|
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.
|
|
72
|
-
"@typescript-eslint/parser": "^8.
|
|
73
|
-
"eslint": "^9.
|
|
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
|