scimgateway 5.3.0 → 5.3.2

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
@@ -16,7 +16,7 @@ Validated through IdP's:
16
16
 
17
17
  Latest news:
18
18
 
19
- - [SCIM Bulk Operations](https://datatracker.ietf.org/doc/html/rfc7644#section-3.7) now supported
19
+ - [Bulk Operations](https://datatracker.ietf.org/doc/html/rfc7644#section-3.7) now supported
20
20
  - Remote real-time log subscription for monitoring and centralized logging
21
21
  using browser and url: `https://<host>/logger`
22
22
  `curl -N https://<host>/logger -u user:password`
@@ -159,7 +159,7 @@ If internet connection is blocked, we could install on another machine and copy
159
159
  >Tip, take a look at bun test scripts located in `node_modules\scimgateway\test\lib`
160
160
 
161
161
  > If using Node.js instead of Bun, scimgateway must be downloaded from github because Node.js does not support native typescript used by modules. Startup will then be:
162
- node --experimental-strip-types c:\my-scimgateway\index.ts
162
+ `node --experimental-strip-types c:\my-scimgateway\index.ts`
163
163
 
164
164
  #### Upgrade SCIM Gateway
165
165
 
@@ -1351,10 +1351,10 @@ Plugins should have following initialization:
1351
1351
  // start - mandatory plugin initialization
1352
1352
  const ScimGateway: typeof import('scimgateway').ScimGateway = await (async () => {
1353
1353
  try {
1354
- return (await import('scimgateway')).ScimGateway
1354
+ return (await import('scimgateway')).ScimGateway
1355
1355
  } catch (err) {
1356
- const source = './scimgateway.ts'
1357
- return (await import(source)).ScimGateway
1356
+ const source = './scimgateway.ts'
1357
+ return (await import(source)).ScimGateway
1358
1358
  }
1359
1359
  })()
1360
1360
  const scimgateway = new ScimGateway()
@@ -1368,10 +1368,10 @@ If using REST, we could also include the HelperRest:
1368
1368
  ...
1369
1369
  const HelperRest: typeof import('scimgateway').HelperRest = await (async () => {
1370
1370
  try {
1371
- return (await import('scimgateway')).HelperRest
1371
+ return (await import('scimgateway')).HelperRest
1372
1372
  } catch (err) {
1373
- const source = './scimgateway.ts'
1374
- return (await import(source)).HelperRest
1373
+ const source = './scimgateway.ts'
1374
+ return (await import(source)).HelperRest
1375
1375
  }
1376
1376
  })()
1377
1377
  ...
@@ -1405,11 +1405,25 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1405
1405
 
1406
1406
  ## Change log
1407
1407
 
1408
+ ### v5.3.2
1409
+
1410
+ [Improved]
1411
+
1412
+ - helper-rest, retry on request error 504 Gateway Timeout
1413
+ - performance micro-optimization on log mask logic
1414
+
1415
+ ### v5.3.1
1416
+
1417
+ [Fixed]
1418
+
1419
+ - Incorrect log masking of SCIM 2.0 PATCH Operations
1420
+ - plugin-ldap, create user/group having DN special character `#` failed on OpenLDAP
1421
+
1408
1422
  ### v5.3.0
1409
1423
 
1410
1424
  [Improved]
1411
1425
 
1412
- - [SCIM Bulk Operations](https://datatracker.ietf.org/doc/html/rfc7644#section-3.7) now supported
1426
+ - [Bulk Operations](https://datatracker.ietf.org/doc/html/rfc7644#section-3.7) now supported
1413
1427
  - Dependencies bump
1414
1428
 
1415
1429
  ### v5.2.5
@@ -699,7 +699,7 @@ export class HelperRest {
699
699
  try { urlObj = new URL(path) } catch (err) { void 0 }
700
700
  let isServiceClient = !urlObj && this._serviceClient[baseEntity] && !this.lock.isLocked() // !isLocked to avoid retry ongoing doRequest with failing getAccessToken()
701
701
  let oAuthTokeErr = statusCode === 401 && this.config_entity[baseEntity].connection?.auth?.type && this.config_entity[baseEntity].connection.auth.type.startsWith('oauth')
702
- if (isServiceClient && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ABORT_ERR' || err.code === 'ETIMEDOUT' || oAuthTokeErr || retryAfter)) {
702
+ if (isServiceClient && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ABORT_ERR' || err.code === 'ETIMEDOUT' || statusCode === 504 || oAuthTokeErr || retryAfter)) {
703
703
  this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
704
704
  if (retryAfter) {
705
705
  this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} throttle/ratelimit error - awaiting ${retryAfter} seconds before automatic retry`)
package/lib/logger.ts CHANGED
@@ -81,6 +81,7 @@ export class Logger {
81
81
  private rotating = false
82
82
  private buffer: string[] = []
83
83
  private reJson: RegExp
84
+ private reJsonPathValue: RegExp
84
85
  private reXml: RegExp
85
86
  private callbacks: Set<(message: any) => Promise<void>> = new Set()
86
87
  private LOG_DIR: string
@@ -128,20 +129,26 @@ export class Logger {
128
129
  if (option.customMasking) this.customMasking = option.customMasking
129
130
  }
130
131
 
131
- let customMaskJson = ''
132
- let customMaskXml = ''
133
- if (this.customMasking && Array.isArray(this.customMasking) && this.customMasking.length > 0) {
134
- customMaskJson = this.customMasking.join('|')
135
- customMaskJson = '|' + customMaskJson
136
- customMaskXml = this.customMasking.join('"?|')
137
- customMaskXml = '|' + customMaskXml + '"?'
138
- }
132
+ let customMask = this.customMasking || []
133
+ if (!Array.isArray(customMask)) customMask = []
134
+ const jsonMaskKeys = ['password', 'access_token', 'client_secret', 'assertion', 'client_assertion']
135
+ const jsonJoinedKeys = jsonMaskKeys.concat(customMask).join('|')
136
+ const xmlMaskKeys = ['credentials', 'PasswordText', 'PasswordDigest', 'password']
137
+ const xmlJoinedKeys = xmlMaskKeys.concat(customMask).join('"?|') + '"?'
138
+
139
139
  this.reJson = new RegExp(
140
- `("(password|access_token|client_secret|assertion|client_assertion|${customMaskJson})"\\s*:\\s*)"([^"]+)"`,
140
+ `("(?:${jsonJoinedKeys})"\\s*:\\s*)"([^"]+)"`,
141
141
  'gi',
142
142
  )
143
+
144
+ // matches "path":"<maskKey>", then finds "value":"<value>" to mask it - SCIM 2.0 PATCH Operations
145
+ this.reJsonPathValue = new RegExp(
146
+ `("path"\\s*:\\s*"(?:${jsonJoinedKeys})"[^{}]*?"value"\\s*:\\s*")([^"]+)(")`,
147
+ 'gi',
148
+ )
149
+
143
150
  this.reXml = new RegExp(
144
- `(<(?:\\w+:)?(credentials"?|PasswordText"?|PasswordDigest"?|password"?|${customMaskXml})[^>]*>)([^<]+)(<\\/(:?\\w+:)?\\2>)`,
151
+ `(<(?:\\w+:)?(${xmlJoinedKeys})[^>]*>)([^<]+)(<\\/(:?\\w+:)?\\2>)`,
145
152
  'gi',
146
153
  )
147
154
 
@@ -164,18 +171,30 @@ export class Logger {
164
171
 
165
172
  private maskSecret(msg: string): string {
166
173
  if (!msg) return msg
174
+
167
175
  // Mask JSON secrets
168
176
  msg = msg.replace(
169
177
  this.reJson,
170
178
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
171
- (_, keyValuePair, key) => `${keyValuePair}"********"`,
179
+ (_, keyValuePair, value) => `${keyValuePair}"******"`,
172
180
  )
181
+
182
+ // Mask JSON path/value secrets (SCIM 2.0 PATCH Operations)
183
+ if (msg.includes('"path"')) {
184
+ msg = msg.replace(
185
+ this.reJsonPathValue,
186
+ (_, prefix, value, suffix) => `${prefix}******${suffix}`,
187
+ )
188
+ }
189
+
173
190
  // Mask XML/Soap secrets
174
191
  // console.log('XML matches found:', msg.match(this.reXml)
175
- msg = msg.replace(
176
- this.reXml,
177
- (_, startTag, tagName, value, endTag) => `${startTag}********${endTag}`,
178
- )
192
+ if (msg.includes('<?xml')) {
193
+ msg = msg.replace(
194
+ this.reXml,
195
+ (_, startTag, tagName, value, endTag) => `${startTag}******${endTag}`,
196
+ )
197
+ }
179
198
 
180
199
  return msg
181
200
  }
@@ -1221,6 +1221,10 @@ const ldapEsc = (str: any) => {
1221
1221
  if (isEsc) c = ')'
1222
1222
  else c = '\\)'
1223
1223
  break
1224
+ case '#':
1225
+ if (isEsc) c = '#'
1226
+ else c = '\\#'
1227
+ break
1224
1228
  }
1225
1229
  newStr += c
1226
1230
  }
@@ -570,10 +570,18 @@ export class ScimGateway {
570
570
  if (authType === 'Basic') [userName] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
571
571
  if (!userName && authType === 'Bearer') userName = 'token'
572
572
  let outbound = ctx.response.body
573
- if (typeof outbound === 'string' && outbound.length > 1000) {
574
- outbound = outbound.slice(0, 1000)
575
- outbound += '...truncated because of length'
573
+
574
+ if (typeof outbound === 'string' && outbound.length > 1500 && outbound.includes('"Resources":')) {
575
+ try {
576
+ const o = JSON.parse(outbound)
577
+ if (o?.Resources?.length > 1) {
578
+ o.Resources = [o.Resources[0]]
579
+ o.Resources.push({ loggerComment: '===REST OF OBJECTS TRUNCATED BECAUSE OF LOG LENGTH===' })
580
+ outbound = JSON.stringify(o)
581
+ }
582
+ } catch (err) {}
576
583
  }
584
+
577
585
  if (ctx.response.status && (ctx.response.status < 200 || ctx.response.status > 299)) {
578
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}`)
579
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}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.3.0",
3
+ "version": "5.3.2",
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)",