scimgateway 4.2.9 → 4.2.11

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 CHANGED
@@ -1169,6 +1169,18 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1169
1169
 
1170
1170
  ## Change log
1171
1171
 
1172
+ ### v4.2.11
1173
+
1174
+ [Added]
1175
+
1176
+ - Plugin can set error statusCode returned by scimgateway through error message. Error message must then contain string `"statusCode":xxx` where xxx is HTTP status code e.g., 401. Plugin using REST will have statusCode automatically included in error message thrown by plugin. This could be useful for auth.PassThrough.
1177
+
1178
+ ### v4.2.10
1179
+
1180
+ [Fixed]
1181
+
1182
+ - plugin-ldap broken after dependencies bump of ldapjs (from 2.x.x to 3.x.x) in version 4.2.7
1183
+
1172
1184
  ### v4.2.9
1173
1185
 
1174
1186
  [Fixed]
@@ -110,15 +110,11 @@ if (config.useSID_id && config.map.group) {
110
110
  }
111
111
  if (config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) { // support mapping different inbound/outbound upn domain names
112
112
  if (config.map.user.userPrincipalName.mapDomain.inbound && config.map.user.userPrincipalName.mapDomain.outbound) {
113
- let inbound = config.map.user.userPrincipalName.mapDomain.inbound
114
- let outbound = config.map.user.userPrincipalName.mapDomain.outbound
115
- inbound = inbound.startsWith('@') ? inbound : '@' + inbound
116
- outbound = outbound.startsWith('@') ? outbound : '@' + outbound
117
- config.upnMapDomain = {
118
- inbound: inbound, // "test.onmicrosoft.com
119
- outbound: outbound // "my-company.com"
120
- }
121
- }
113
+ const inbound = config.map.user.userPrincipalName.mapDomain.inbound
114
+ const outbound = config.map.user.userPrincipalName.mapDomain.outbound
115
+ config.map.user.userPrincipalName.mapDomain.inbound = inbound.startsWith('@') ? inbound : '@' + inbound // "@my-company.com"
116
+ config.map.user.userPrincipalName.mapDomain.outbound = outbound.startsWith('@') ? outbound : '@' + outbound // "@test.onmicrosoft.com
117
+ } else delete config.map.user.userPrincipalName.mapDomain
122
118
  }
123
119
 
124
120
  // =================================================
@@ -1034,13 +1030,12 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1034
1030
  let client = null
1035
1031
 
1036
1032
  const options = scimgateway.copyObj(ldapOptions)
1037
- if (config.upnMapDomain) {
1038
- for (const key in options) {
1039
- if ((typeof options[key] === 'string') && options[key].includes(config.upnMapDomain.inbound)) {
1040
- const old = options[key]
1041
- options[key] = options[key].replace(config.upnMapDomain.inbound, config.upnMapDomain.outbound)
1042
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] inbound upnMapDomain ${old} => ${options[key]}`)
1043
- }
1033
+ // support having different upn-domain on IdP and target
1034
+ if (options.modification && options.modification.userPrincipalName && config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) {
1035
+ if (options.modification.userPrincipalName.endsWith(config.map.user.userPrincipalName.mapDomain.outbound)) {
1036
+ const old = options.modification.userPrincipalName
1037
+ options.modification.userPrincipalName = options.modification.userPrincipalName.replace(config.map.user.userPrincipalName.mapDomain.outbound, config.map.user.userPrincipalName.mapDomain.inbound)
1038
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] inbound upnMapDomain ${old} => ${options.modification.userPrincipalName}`)
1044
1039
  }
1045
1040
  }
1046
1041
 
@@ -1055,37 +1050,45 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1055
1050
  if (err) {
1056
1051
  return reject(err)
1057
1052
  }
1053
+
1058
1054
  search.on('searchEntry', (entry) => {
1059
- if (entry.attributes) {
1060
- entry.attributes.find((el, i) => {
1061
- if (['objectSid', 'objectGUID'].includes(el.type)) { // assume Active Directory - can't use default utf-8 when attribute value is hex
1062
- const b = Buffer.from(el.buffers[0], 'hex')
1063
- if (el.type === 'objectSid') {
1064
- const sidStr = convertSidToString(b) // using string: S-1-5-21-2657077294-4200173015-2627628055-1255
1065
- if (!sidStr) throw new Error(`doRequest() error: failed to convert SID ${b.toString('hex')} to string}`)
1066
- entry.attributes[i]._vals = [sidStr]
1067
- } else {
1068
- entry.attributes[i]._vals = [b.toString('base64')] // using base64: nitWLrhokUqKl1DywiavXg==
1069
- }
1070
- } else if (el.type === 'userPrincipalName' && config.upnMapDomain) {
1071
- const val = Buffer.from(el.buffers[0], 'hex').toString('utf8')
1072
- const old = val
1073
- entry.attributes[i]._vals = [val.replace(config.upnMapDomain.outbound, config.upnMapDomain.inbound)]
1074
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] outbound upnMapDomain ${old} => ${entry.attributes[i]._vals}`)
1075
- }
1076
- return undefined
1077
- })
1055
+ if (!entry.pojo || !entry.pojo.attributes) return
1056
+ const obj = { dn: entry.pojo.objectName }
1057
+ entry.pojo.attributes.map((el) => {
1058
+ if (el.values.length > 1) obj[el.type] = el.values
1059
+ else obj[el.type] = el.values[0]
1060
+ return null
1061
+ })
1062
+ // objectSid/objectGUID - assume Active Directory - can't use default utf-8 when attribute value is hex
1063
+ if (obj.objectSid) {
1064
+ const b = Buffer.from(obj.objectSid, 'utf-8')
1065
+ const sidStr = convertSidToString(b) // using string: S-1-5-21-2657077294-4200173015-2627628055-1255
1066
+ if (!sidStr) throw new Error(`doRequest() error: failed to convert SID ${b.toString('hex')} to string}`)
1067
+ obj.objectSid = sidStr
1068
+ }
1069
+ if (obj.objectGUID) {
1070
+ const b = Buffer.from(obj.objectGUID, 'utf-8')
1071
+ obj.objectGUID = b.toString('base64') // using base64: nitWLrhokUqKl1DywiavXg==
1072
+ }
1073
+ if (obj.userPrincipalName && config.map.user.userPrincipalName && config.map.user.userPrincipalName.mapDomain) {
1074
+ if (obj.userPrincipalName.endsWith(config.map.user.userPrincipalName.mapDomain.inbound)) {
1075
+ const old = obj.userPrincipalName
1076
+ obj.userPrincipalName = obj.userPrincipalName.replace(config.map.user.userPrincipalName.mapDomain.inbound, config.map.user.userPrincipalName.mapDomain.outbound)
1077
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] outbound upnMapDomain ${old} => ${obj.userPrincipalName}`)
1078
+ }
1078
1079
  }
1079
- results.push(entry.object)
1080
+ results.push(obj)
1080
1081
  })
1081
1082
 
1082
1083
  search.on('page', (entry, cb) => {
1083
1084
  // if (cb) cb() // pagePause = true gives callback
1084
1085
  })
1086
+
1085
1087
  search.on('error', (err) => {
1086
1088
  if (err.message.includes('LdapErr: DSID-0C0909F2') || err.message.includes('NO_OBJECT')) return resolve([]) // object not found when using base <SID=...> or <GUID=...> ref. objectSid/objectGUID
1087
1089
  reject(err)
1088
1090
  })
1091
+
1089
1092
  search.on('end', (_) => { resolve(results) })
1090
1093
  })
1091
1094
  })
@@ -1094,7 +1097,22 @@ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1094
1097
  case 'modify':
1095
1098
  result = await new Promise((resolve, reject) => {
1096
1099
  const dn = base
1097
- client.modify(dn, options, (err) => {
1100
+ const changes = []
1101
+ for (const key in options.modification) {
1102
+ const mod = {}
1103
+ mod.type = key
1104
+ if (Array.isArray(options.modification[key])) mod.values = options.modification[key]
1105
+ else {
1106
+ if (typeof options.modification[key] === 'string') mod.values = [options.modification[key]]
1107
+ else mod.values = [options.modification[key].toString()]
1108
+ }
1109
+ const change = new ldap.Change({
1110
+ operation: options.operation || 'replace',
1111
+ modification: mod // { type: "givenName", values: ["Joe"] }
1112
+ })
1113
+ changes.push(change)
1114
+ }
1115
+ client.modify(dn, changes, (err) => {
1098
1116
  if (err) {
1099
1117
  if (options.operation && options.operation === 'add' && options.modification && options.modification.member) {
1100
1118
  if (err.message.includes('ENTRY_EXISTS')) return resolve() // add already existing group to user
@@ -318,22 +318,35 @@ const ScimGateway = function () {
318
318
  if (!userName && authType === 'Bearer') userName = 'token'
319
319
  if (ctx.request.url !== '/favicon.ico') {
320
320
  if (ctx.response.status < 200 || ctx.response.status > 299) {
321
- let isEndpointAccessDenied = false
321
+ // statusCode check in logResult method...
322
+ // "statusCode":xxx in error messages let plugin set error statusCode returned by scimgateway
323
+ let pluginStatusCode = 0
324
+ const reJson = '^.*"(statusCode)" *: *([0-9][0-9][0-9]).*'
325
+ const rePattern = new RegExp(reJson, 'i')
322
326
  if (res.body.detail) {
323
- if (res.body.detail.includes('\"statusCode\":401')) isEndpointAccessDenied= true // eslint-disable-line
327
+ const arrMatches = res.body.detail.match(rePattern)
328
+ if (Array.isArray(arrMatches) && arrMatches.length === 3) {
329
+ pluginStatusCode = parseInt(arrMatches[2])
330
+ }
324
331
  } else if (res.body.Errors) {
325
- if (Array.isArray(res.body.Errors) && res.body.Errors[0].description && res.body.Errors[0].description.includes('\"statusCode\":401')) { // eslint-disable-line
326
- isEndpointAccessDenied = true
332
+ if (Array.isArray(res.body.Errors) && res.body.Errors[0].description && res.body.Errors[0].description) {
333
+ const arrMatches = res.body.Errors[0].description.match(rePattern)
334
+ if (Array.isArray(arrMatches) && arrMatches.length === 3) {
335
+ pluginStatusCode = parseInt(arrMatches[2])
336
+ }
327
337
  }
328
338
  }
329
- if (isEndpointAccessDenied) { // don't reveal original SCIM error message details related to access denied (e.g. using Auth PassThrough)
330
- ctx.response.set('Content-Type', 'application/json; charset=utf-8')
331
- ctx.response.status = 401 // ctx.response.message becomes default 'Unauthorized'
332
- ctx.response.body = { error: 'Access denied' }
339
+ if (pluginStatusCode > 0) {
340
+ ctx.response.status = pluginStatusCode // auto change ctx.response.message
333
341
  res.statusCode = ctx.response.status
334
342
  res.statusMessage = ctx.response.message
335
- res.body = ctx.response.body
343
+ if (pluginStatusCode === 401 || pluginStatusCode === 403) { // don't reveal original SCIM error message details related to access denied (e.g. using Auth PassThrough)
344
+ ctx.response.set('Content-Type', 'application/json; charset=utf-8')
345
+ ctx.response.body = { error: 'Access denied' }
346
+ res.body = ctx.response.body
347
+ }
336
348
  }
349
+ // back to logResult...
337
350
  logger.error(`${gwName}[${pluginName}] ${ellapsed} ${ctx.request.ipcli} ${userName} ${ctx.request.method} ${ctx.request.href} Inbound = ${JSON.stringify(ctx.request.body)} Outbound = ${JSON.stringify(res)}${(config.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
338
351
  } else logger.info(`${gwName}[${pluginName}] ${ellapsed} ${ctx.request.ipcli} ${userName} ${ctx.request.method} ${ctx.request.href} Inbound = ${JSON.stringify(ctx.request.body)} Outbound = ${JSON.stringify(res)}${(config.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
339
352
  requestCounter += 1 // logged on exit (not win process termination)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.2.9",
3
+ "version": "4.2.11",
4
4
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
5
5
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
6
6
  "homepage": "https://elshaug.xyz",