scimgateway 5.4.1 → 5.4.3

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
@@ -394,7 +394,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
394
394
 
395
395
  - **log.loglevel.console** - off, debug, info, warn or error. Default off. Output to stdout and errors to stderr
396
396
 
397
- - **log.loglevel.push** - off, debug, info, warn or error. Default info. Push to stream used by remote real-time log subscription
397
+ - **log.loglevel.push** - debug, info, warn or error. Default info. Push to stream used by remote real-time log subscription
398
398
 
399
399
  - **log.logDirectory** - custom defined log directory e.g. `/var/log/scimgateway` that will override default `<scimgateway path>/logs`. If not exist it will be created.
400
400
 
@@ -746,13 +746,18 @@ Please see code editor method HelperRest doRequest() IntelliSense for type and o
746
746
  ### Configuration notes - Remote real-time log subscription
747
747
  Using remote real-time log subscription we may implement custom logic like monitoring and centralized logging
748
748
 
749
- - using browser and url: https://host/logger
750
- - curl -Ns https://host/logger -u gwread:password | sed 's/\xE2\x80\x8B//g'
751
- - curl -Ns https://host/logger -H "Authorization: Bearer secret" | sed 's/\xE2\x80\x8B//g'
752
- (-s and sed to ignore keep-alive character)
749
+ - browser and url: https://host/logger
750
+ - curl with -u or -H "Authorization: Bearer secret"
751
+ ```
752
+ curl -Ns http://localhost:8880/logger -u gwadmin:password | awk '
753
+ /^data: / {sub(/^data: /,""); printf "%s", $0; last=1; next}
754
+ /^$/ {if (last) print ""; last=0}
755
+ '
756
+ ```
753
757
  - custom client API (see example below)
754
758
  - not supported by Azure Relay
755
759
 
760
+
756
761
  We may configure read-only user/secret for log collection purpose
757
762
 
758
763
  "auth": {
@@ -792,67 +797,57 @@ Example using debug loglevel:
792
797
  Example code implementing remote real-time log subscription and custom message handling
793
798
 
794
799
  ```
795
- // startup: bun <scriptname.ts>
796
- // update url (ws or wss) and the auth according to environment used
797
- const url = 'ws://localhost:8880/logger'
798
- const auth = 'Basic ' + btoa('gwadmin' + ':' + 'password') // const auth = 'Bearer ' + 'secret'
799
-
800
- const tls: any = {}
801
- if (url.startsWith('wss:')) {
802
- tls.ca = [Bun.file('/path/to/self-signed-cert.pem')], // only needed for self-signed certs
803
- tls.rejectUnauthorized = false
804
- }
805
-
806
- // messageHandler implements message handling and custom logic
807
- // could also use JSON.parse(message) and granular filtering on log "level"
800
+ //
801
+ // usage: bun <scriptname.ts>
802
+ // update url and the auth according to environment used
803
+ //
804
+ const username = "gwadmin"
805
+ const password = "password"
806
+ const url = "http://localhost:8880/logger"
807
+
808
+ const headers = new Headers({
809
+ Authorization: "Basic " + btoa(`${username}:${password}`),
810
+ Accept: "text/event-stream"
811
+ })
812
+
813
+ // message handling and custom logic
814
+ // we could also do JSON.parse(message) and granular filtering on log "level"
808
815
  const messageHandler = async (message: string) => {
809
816
  console.log(message)
810
817
  }
811
818
 
812
- const startWebSocket = async () => {
813
- try {
814
- const ws = new WebSocket(url, {
815
- headers: {
816
- Authorization: auth,
817
- },
818
- tls,
819
- })
820
-
821
- // message is received
822
- ws.addEventListener("message", event => {
823
- messageHandler(event.data)
824
- })
825
-
826
- // socket opened
827
- ws.addEventListener("open", event => {
819
+ async function startSSE() {
820
+ while (true) {
821
+ try {
822
+ const resp = await fetch(url, { headers });
823
+ if (!resp.ok || !resp.body) {
824
+ console.error(`❌ Response error: ${resp.status} ${resp.statusText}`)
825
+ await Bun.sleep(10_000)
826
+ continue
827
+ }
828
828
  console.log('✅ Now awaiting log events...\n')
829
- })
830
-
831
- // socket closed
832
- ws.addEventListener("close", event => {
833
- let addInfo = ''
834
- if (event.code === 1002) addInfo = ' => most likely authentication failure?'
835
- console.warn(`⚠️ Connection closed (${event.code}): ${event.reason || 'no reason'}${addInfo}`)
836
- retry()
837
- })
838
-
839
- // error handler
840
- ws.addEventListener("error", event => {
841
- // console.error('❌ WebSocket error:', event.message)
842
- })
843
-
844
- } catch (err: any) {
845
- console.error('❌ Unexpected error:', err)
846
- }
847
- }
848
829
 
849
- const retry = async () => {
850
- console.log('🔁 Retry in 10 seconds...')
851
- await Bun.sleep(10 * 1000)
852
- startWebSocket()
830
+ const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader()
831
+
832
+ while (true) {
833
+ const { value, done } = await reader.read()
834
+ if (done) break
835
+ if (!value.startsWith('data: ')) continue
836
+ const i = value.indexOf("\n\n")
837
+ if (i < 1) continue
838
+ const msg = value.slice(6, i)
839
+ messageHandler(msg)
840
+ }
841
+ console.error("⚠️ Connection closed");
842
+ await Bun.sleep(10_000)
843
+ } catch (err: any) {
844
+ console.error("❌ Connection error:", err?.message || err)
845
+ await Bun.sleep(10_000)
846
+ }
847
+ }
853
848
  }
854
849
 
855
- startWebSocket()
850
+ startSSE()
856
851
  ```
857
852
 
858
853
  ### Configuration notes - Azure Relay
@@ -1473,6 +1468,25 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1473
1468
 
1474
1469
  ## Change log
1475
1470
 
1471
+ ### v5.4.3
1472
+
1473
+ [Fixed]
1474
+
1475
+ - helper-rest, fixed an issue introduced in v5.3.8 that caused problems using OAuth
1476
+
1477
+ [Improved]
1478
+
1479
+ - Remote real-time logger
1480
+
1481
+ ### v5.4.2
1482
+
1483
+ [Improved]
1484
+
1485
+ - baseEntity included as json-key in logs
1486
+ - Remote real-time logger now supports baseEntity. `http(s)://host/logger` gives all log entries for plugin. `http(s)://host/<baseEntity>/logger` gives only log entries for the baseEntity used.
1487
+
1488
+ Note, using `baseEntity` is optional. This is a parameter used for multi tenant or multi endpoint solutions. We could create several endpoint configurations having unique baseEntity. Also note that we can configure auth linked to baseEntity including readOnly.
1489
+
1476
1490
  ### v5.4.1
1477
1491
 
1478
1492
  [Improved]
@@ -578,7 +578,7 @@ export class HelperRest {
578
578
  options.headers['Authorization'] = 'Basic ' + Buffer.from(`${o.auth?.options?.username}:${o.auth?.options?.password}`).toString('base64')
579
579
  delete o.auth
580
580
  }
581
- options = utils.extendObj(o?.connection?.options, options)
581
+ options = utils.extendObj(options, o)
582
582
  }
583
583
 
584
584
  const cli: any = {}
package/lib/logger.ts CHANGED
@@ -294,32 +294,33 @@ export class Logger {
294
294
  * @param level log level
295
295
  * @param message the message that will be logged
296
296
  */
297
- private async log(level: 'debug' | 'info' | 'warn' | 'error', message: string) {
297
+ private async log(level: 'debug' | 'info' | 'warn' | 'error', message: string, obj?: Record<string, any>) {
298
298
  const time = new Date().toISOString()
299
299
  message = this.maskSecret(message)
300
300
  const msgObj: Record<string, any> = {
301
301
  time,
302
- category: this.category,
303
302
  level,
303
+ category: this.category,
304
+ ...(obj || {}),
304
305
  message,
305
306
  }
306
307
  this.logChannel.publish(msgObj)
307
308
  }
308
309
 
309
- public debug(message: string) {
310
- this.log('debug', message)
310
+ public debug(message: string, obj?: Record<string, any>) {
311
+ this.log('debug', message, obj)
311
312
  }
312
313
 
313
- public info(message: string) {
314
- this.log('info', message)
314
+ public info(message: string, obj?: Record<string, any>) {
315
+ this.log('info', message, obj)
315
316
  }
316
317
 
317
- public warn(message: string) {
318
- this.log('warn', message)
318
+ public warn(message: string, obj?: Record<string, any>) {
319
+ this.log('warn', message, obj)
319
320
  }
320
321
 
321
- public error(message: string) {
322
- this.log('error', message)
322
+ public error(message: string, obj?: Record<string, any>) {
323
+ this.log('error', message, obj)
323
324
  }
324
325
 
325
326
  /**
@@ -586,11 +586,11 @@ export class ScimGateway {
586
586
 
587
587
  if (ctx.response.status && (ctx.response.status < 200 || ctx.response.status > 299)) {
588
588
  if (ctx.response.status === 412 || ctx.response.status === 304) {
589
- 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}`)
589
+ 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}`, { baseEntity: ctx?.routeObj?.baseEntity })
590
590
  } else if (ctx.response.status === 404) {
591
- 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}`)
592
- } 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}`)
593
- } 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}`)
591
+ 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}`, { baseEntity: ctx?.routeObj?.baseEntity })
592
+ } 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}`, { baseEntity: ctx?.routeObj?.baseEntity })
593
+ } 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}`, { baseEntity: ctx?.routeObj?.baseEntity })
594
594
  }
595
595
 
596
596
  // start auth methods - used by auth
@@ -793,12 +793,12 @@ export class ScimGateway {
793
793
  if (authType.length < 1) err = new Error(`${ctx.request.url} request is missing authentication information`)
794
794
  else {
795
795
  err = new Error(`${ctx.request.url} request having unsupported authentication or plugin configuration is missing`)
796
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] request authToken = ${authToken}`)
797
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] request jwt.decode(authToken) = ${JSON.stringify(jwt.decode(authToken))}`)
796
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] request authToken = ${authToken}`, { baseEntity: ctx?.routeObj?.baseEntity })
797
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] request jwt.decode(authToken) = ${JSON.stringify(jwt.decode(authToken))}`, { baseEntity: ctx?.routeObj?.baseEntity })
798
798
  }
799
799
  if (authType === 'Bearer') ctx.response.headers.set('WWW-Authenticate', 'Bearer realm=""')
800
800
  else if (found.Basic) ctx.response.headers.set('WWW-Authenticate', 'Basic realm=""')
801
- if (ctx.request.url !== '/favicon.ico' && !ctx.request.url.startsWith('/apple-touch-icon')) logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${err.message}`)
801
+ if (ctx.request.url !== '/favicon.ico' && !ctx.request.url.startsWith('/apple-touch-icon')) logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
802
802
  return false
803
803
  } catch (err: any) {
804
804
  if (authType === 'Bearer') {
@@ -818,9 +818,9 @@ export class ScimGateway {
818
818
  } else ctx.response.headers.set('WWW-Authenticate', 'Basic realm=""')
819
819
  if (pwErrCount < 3) {
820
820
  pwErrCount += 1
821
- logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ctx.request.url} ${err.message}`)
821
+ logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ctx.request.url} ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
822
822
  } else { // delay brute force attempts
823
- logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ctx.request.url} ${err.message} => delaying response with 2 minutes to prevent brute force`)
823
+ logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ctx.request.url} ${err.message} => delaying response with 2 minutes to prevent brute force`, { baseEntity: ctx?.routeObj?.baseEntity })
824
824
  await new Promise((resolve) => {
825
825
  setTimeout(() => { resolve(null) }, 1000 * 60 * 2)
826
826
  })
@@ -833,7 +833,6 @@ export class ScimGateway {
833
833
  const ipAllowList = (ipAddr: string): boolean => {
834
834
  if (ipAllowListChecker === undefined) return true
835
835
  if (ipAllowListChecker(ipAddr) === true) return true // if proxy, prereq: request includes header X-Forwarded-For
836
- logger.debug(`${gwName}[${pluginName}] client ip ${ipAddr} not in ipAllowList`)
837
836
  return false
838
837
  }
839
838
 
@@ -879,27 +878,31 @@ export class ScimGateway {
879
878
  const getHandlerLoggerSSE = async (ctx: Context) => {
880
879
  const levelInt = logger.levelToInt(this.config?.scimgateway?.log?.loglevel?.push || 'info')
881
880
  const encoder = new TextEncoder()
881
+ logger.info(`${gwName}[${pluginName}] remote logger connected from ip address ${ctx.ip}`, { baseEntity: ctx?.routeObj?.baseEntity })
882
882
 
883
883
  return new Response(
884
884
  new ReadableStream({
885
885
  start(controller) {
886
- controller.enqueue(encoder.encode(`\u200B`))
886
+ controller.enqueue(encoder.encode(`: keep-alive\n\n`))
887
887
 
888
888
  const sub = async (msgObj: Record<string, any>) => {
889
889
  if (logger.levelToInt(msgObj.level) < levelInt) return
890
- controller.enqueue(encoder.encode(`${JSON.stringify(msgObj)}\n`))
890
+ if (ctx?.routeObj?.baseEntity !== 'undefined') { // if using baseEntity e.g. <host>/company1/logger, only include corresponding baseEntity logentries
891
+ if (ctx?.routeObj?.baseEntity !== msgObj.baseEntity) return
892
+ }
893
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(msgObj)}\n\n`))
891
894
  }
892
895
  logger.subscribe(sub)
893
896
 
894
897
  const keepAliveInterval = setInterval(() => {
895
- controller.enqueue(encoder.encode(`\u200B`)) // invisible keep-alive
898
+ controller.enqueue(encoder.encode(`: keep-alive\n\n`))
896
899
  }, 10000)
897
900
 
898
901
  const cleanup = () => {
899
902
  clearInterval(keepAliveInterval)
900
903
  logger.unsubscribe(sub)
901
904
  controller.close()
902
- logger.info(`${gwName}[${pluginName}] remote logger disconnected from ip address ${ctx.ip}`)
905
+ logger.info(`${gwName}[${pluginName}] remote logger disconnected from ip address ${ctx.ip}`, { baseEntity: ctx?.routeObj?.baseEntity })
903
906
  }
904
907
 
905
908
  ctx.request.signal.onabort = cleanup // Bun
@@ -921,9 +924,9 @@ export class ScimGateway {
921
924
 
922
925
  // oauth token request, POST /oauth/token
923
926
  const postHandlerOauthToken = async (ctx: Context) => {
924
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request`)
927
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request`, { baseEntity: ctx?.routeObj?.baseEntity })
925
928
  if (!found.BearerOAuth) {
926
- logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request, but plugin is missing auth.bearerOAuth configuration`)
929
+ logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request, but plugin is missing auth.bearerOAuth configuration`, { baseEntity: ctx?.routeObj?.baseEntity })
927
930
  ctx.response.status = 500
928
931
  return
929
932
  }
@@ -931,7 +934,7 @@ export class ScimGateway {
931
934
  try {
932
935
  if (!jsonBody) throw new Error('missing body')
933
936
  if (typeof jsonBody !== 'object') { // might have application/x-www-form-urlencoded or multipart/form-data body, but incorrect Content-Type header
934
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] continue request validation even though incorrect body vs header Content-Type: ${ctx.request.headers.get('content-type')}`)
937
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] continue request validation even though incorrect body vs header Content-Type: ${ctx.request.headers.get('content-type')}`, { baseEntity: ctx?.routeObj?.baseEntity })
935
938
  let body = utils.formUrlEncodedToJSON(jsonBody)
936
939
  if (Object.keys(body).length < 1) {
937
940
  body = utils.formDataMultipartToJSON(jsonBody)
@@ -942,7 +945,7 @@ export class ScimGateway {
942
945
  }
943
946
  jsonBody = utils.copyObj(jsonBody) // no changes to original
944
947
  } catch (err: any) {
945
- logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request error: ${err.message}`)
948
+ logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request error: ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
946
949
  ctx.response.status = 401
947
950
  return
948
951
  }
@@ -1001,7 +1004,7 @@ export class ScimGateway {
1001
1004
  }
1002
1005
 
1003
1006
  if (err) {
1004
- logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request client_id: ${jsonBody ? jsonBody.client_id : ''} error: ${errDescr}`)
1007
+ logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request client_id: ${jsonBody ? jsonBody.client_id : ''} error: ${errDescr}`, { baseEntity: ctx?.routeObj?.baseEntity })
1005
1008
  ctx.response.status = 401
1006
1009
  const errMsg = {
1007
1010
  error: err,
@@ -1063,12 +1066,12 @@ export class ScimGateway {
1063
1066
  value: id,
1064
1067
  }
1065
1068
 
1066
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Get ${handle.description}] ${getObj.attribute}=${getObj.value}`)
1069
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Get ${handle.description}] ${getObj.attribute}=${getObj.value}`, { baseEntity: ctx?.routeObj?.baseEntity })
1067
1070
 
1068
1071
  try {
1069
1072
  const ob = utils.copyObj(getObj)
1070
1073
  const attributes = ctx.query.attributes ? ctx.query.attributes.split(',').map((item: string) => item.trim()) : []
1071
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`)
1074
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
1072
1075
  let res = await (this as any)[handle.getMethod](baseEntity, ob, [], ctx.passThrough)
1073
1076
 
1074
1077
  let scimdata: { [key: string]: any } = {
@@ -1273,7 +1276,7 @@ export class ScimGateway {
1273
1276
 
1274
1277
  let info = ''
1275
1278
  if (getObj.operator === 'eq' && ['id', 'userName', 'externalId', 'displayName', 'members.value'].includes(getObj.attribute)) info = ` ${getObj.attribute}=${getObj.value}`
1276
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Get ${handle.description}s]${info}`)
1279
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Get ${handle.description}s]${info}`, { baseEntity: ctx?.routeObj?.baseEntity })
1277
1280
  try {
1278
1281
  getObj.startIndex = ctx.query.startIndex ? parseInt(ctx.query.startIndex) : undefined
1279
1282
  getObj.count = ctx.query.count ? parseInt(ctx.query.count) : undefined
@@ -1309,7 +1312,7 @@ export class ScimGateway {
1309
1312
  }
1310
1313
  const chunk = 5
1311
1314
  const chunkRes: Record<string, any>[] = []
1312
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} with chunks and awaiting result`)
1315
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} with chunks and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
1313
1316
  do {
1314
1317
  const arrChunk = getObjArr.splice(0, chunk)
1315
1318
  const results = await Promise.allSettled(arrChunk.map(o => getObj(o))) as { status: 'fulfilled' | 'rejected', reason: any, value: any }[] // processing max chunk async
@@ -1328,7 +1331,7 @@ export class ScimGateway {
1328
1331
  }
1329
1332
 
1330
1333
  if (!res) { // standard
1331
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`)
1334
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
1332
1335
  res = await (this as any)[handle.getMethod](baseEntity, obj, [], ctx.passThrough)
1333
1336
  }
1334
1337
  // check for user attribute groups and include if needed
@@ -1392,7 +1395,7 @@ export class ScimGateway {
1392
1395
  const postHandler = async (ctx: Context) => {
1393
1396
  const handle = handler[ctx.routeObj.handle]
1394
1397
  const baseEntity = ctx.routeObj.baseEntity
1395
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Create ${handle.description}]`)
1398
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Create ${handle.description}]`, { baseEntity: ctx?.routeObj?.baseEntity })
1396
1399
  let jsonBody = ctx.request.body
1397
1400
  try {
1398
1401
  if (!jsonBody) throw new Error('missing body')
@@ -1422,9 +1425,9 @@ export class ScimGateway {
1422
1425
  return
1423
1426
  }
1424
1427
 
1425
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] POST ${ctx.origin + ctx.path} body=${JSON.stringify(jsonBody)}`)
1428
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] POST ${ctx.origin + ctx.path} body=${JSON.stringify(jsonBody)}`, { baseEntity: ctx?.routeObj?.baseEntity })
1426
1429
  const [scimdata, err] = utilsScim.convertedScim(jsonBody, this.multiValueTypes)
1427
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] convertedBody=${JSON.stringify(scimdata)}`)
1430
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] convertedBody=${JSON.stringify(scimdata)}`, { baseEntity: ctx?.routeObj?.baseEntity })
1428
1431
  if (err) {
1429
1432
  ctx.response.status = 500
1430
1433
  const [e, customErrorCode] = utilsScim.jsonErr(this.config.scimgateway.scim.version, pluginName, ctx.response.status, err)
@@ -1444,7 +1447,7 @@ export class ScimGateway {
1444
1447
  delete scimdata.groups
1445
1448
  }
1446
1449
  }
1447
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.createMethod} and awaiting result`)
1450
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.createMethod} and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
1448
1451
  const res = await (this as any)[handle.createMethod](baseEntity, scimdata, ctx.passThrough)
1449
1452
  for (const key in res) { // merge any result e.g: {'id': 'xxxx'}
1450
1453
  jsonBody[key] = res[key]
@@ -1531,7 +1534,7 @@ export class ScimGateway {
1531
1534
  ctx.response.body = JSON.stringify(e)
1532
1535
  return
1533
1536
  }
1534
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Delete ${handle.description}] id=${id}`)
1537
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Delete ${handle.description}] id=${id}`, { baseEntity: ctx?.routeObj?.baseEntity })
1535
1538
 
1536
1539
  try {
1537
1540
  if (handle.deleteMethod === 'deleteUser') {
@@ -1547,7 +1550,7 @@ export class ScimGateway {
1547
1550
  })) // result not handled - ignore any failures
1548
1551
  }
1549
1552
  }
1550
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.deleteMethod} and awaiting result`)
1553
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.deleteMethod} and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
1551
1554
  await (this as any)[handle.deleteMethod](baseEntity, id, ctx.passThrough)
1552
1555
  ctx.response.status = 204
1553
1556
  } catch (err: any) {
@@ -1589,7 +1592,7 @@ export class ScimGateway {
1589
1592
  return
1590
1593
  }
1591
1594
 
1592
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Modify ${handle.description}] id=${id}`)
1595
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Modify ${handle.description}] id=${id}`, { baseEntity: ctx?.routeObj?.baseEntity })
1593
1596
 
1594
1597
  const eTagIfMatch = ctx.request.headers.get('if-match')?.split(',').map((item: string) => item.trim()).filter(Boolean)
1595
1598
  const eTagIfNoneMatch = ctx.request.headers.get('if-none-match')?.split(',').map((item: string) => item.trim()).filter(Boolean)
@@ -1597,7 +1600,7 @@ export class ScimGateway {
1597
1600
  let eTag = ''
1598
1601
  if (handle.getMethod === handler.users.getMethod || handle.getMethod === handler.groups.getMethod) { // getUsers or getGroups implemented
1599
1602
  const ob = { attribute: 'id', operator: 'eq', value: id }
1600
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`)
1603
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
1601
1604
  const res = await (this as any)[handle.getMethod](baseEntity, ob, [], ctx.passThrough)
1602
1605
  if (res) {
1603
1606
  if (res.Resources && Array.isArray(res.Resources)) {
@@ -1629,7 +1632,7 @@ export class ScimGateway {
1629
1632
  let scimdata: any, err: any
1630
1633
  if (jsonBody.Operations) [scimdata, err] = utilsScim.convertedScim20(jsonBody, this.multiValueTypes) // v2.0
1631
1634
  else [scimdata, err] = utilsScim.convertedScim(jsonBody, this.multiValueTypes) // v1.1
1632
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] convertedBody=${JSON.stringify(scimdata)}`)
1635
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] convertedBody=${JSON.stringify(scimdata)}`, { baseEntity: ctx?.routeObj?.baseEntity })
1633
1636
  if (err) {
1634
1637
  ctx.response.status = 500
1635
1638
  const [e, customErrorCode] = utilsScim.jsonErr(this.config.scimgateway.scim.version, pluginName, ctx.response.status, err)
@@ -1655,7 +1658,7 @@ export class ScimGateway {
1655
1658
  if (Array.isArray(scimdata.members) && scimdata.members.length === 0 && handle.modifyMethod === 'modifyGroup') {
1656
1659
  res = await replaceUsrGrp(ctx.routeObj.handle, baseEntity, id, scimdata, this.config.scimgateway.scim.usePutSoftSync, ctx.passThrough, undefined)
1657
1660
  } else {
1658
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.modifyMethod} and awaiting result`)
1661
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.modifyMethod} and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
1659
1662
  res = await (this as any)[handle.modifyMethod](baseEntity, id, scimdata, ctx.passThrough)
1660
1663
  }
1661
1664
 
@@ -1680,7 +1683,7 @@ export class ScimGateway {
1680
1683
  return
1681
1684
  }
1682
1685
  const ob = { attribute: 'id', operator: 'eq', value: id }
1683
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`)
1686
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling ${handle.getMethod} and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
1684
1687
  res = await (this as any)[handle.getMethod](baseEntity, ob, [], ctx.passThrough)
1685
1688
  }
1686
1689
 
@@ -1732,9 +1735,9 @@ export class ScimGateway {
1732
1735
  id = decodeURIComponent(id)
1733
1736
 
1734
1737
  // get current object
1735
- logger.debug(`${gwName}[${pluginName}][${baseEntity}] calling ${handle.getMethod} and awaiting result`)
1738
+ logger.debug(`${gwName}[${pluginName}][${baseEntity}] calling ${handle.getMethod} and awaiting result`, { baseEntity })
1736
1739
  const res = await (this as any)[handle.getMethod](baseEntity, { attribute: 'id', operator: 'eq', value: id }, [], ctxPassThrough)
1737
- logger.debug(`${gwName}[${pluginName}][${baseEntity}] "${handle.getMethod}" result: ${res ? JSON.stringify(res) : ''}`)
1740
+ logger.debug(`${gwName}[${pluginName}][${baseEntity}] "${handle.getMethod}" result: ${res ? JSON.stringify(res) : ''}`, { baseEntity })
1738
1741
  let currentObj
1739
1742
  if (res && res.Resources && Array.isArray(res.Resources)) {
1740
1743
  if (res.Resources.length === 1) currentObj = res.Resources[0]
@@ -1792,7 +1795,7 @@ export class ScimGateway {
1792
1795
 
1793
1796
  // update object
1794
1797
  if (Object.keys(scimdata).length > 0) {
1795
- logger.debug(`${gwName}[${pluginName}][${baseEntity}] calling ${handle.modifyMethod} and awaiting result`)
1798
+ logger.debug(`${gwName}[${pluginName}][${baseEntity}] calling ${handle.modifyMethod} and awaiting result`, { baseEntity })
1796
1799
  await (this as any)[handle.modifyMethod](baseEntity, id, scimdata, ctxPassThrough)
1797
1800
  }
1798
1801
 
@@ -1808,7 +1811,7 @@ export class ScimGateway {
1808
1811
  let res: any
1809
1812
  try {
1810
1813
  res = await (this as any)[handler.groups.getMethod](baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }, ['id', 'displayName'], ctxPassThrough)
1811
- logger.debug(`${gwName}[${pluginName}][${baseEntity}] "${handler.groups.getMethod}" result: ${res ? JSON.stringify(res) : ''}`)
1814
+ logger.debug(`${gwName}[${pluginName}][${baseEntity}] "${handler.groups.getMethod}" result: ${res ? JSON.stringify(res) : ''}`, { baseEntity })
1812
1815
  } catch (err) { void 0 } // method may be implemented, but throwing error like groups not supported/implemented
1813
1816
  currentGroups = []
1814
1817
  if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
@@ -1889,7 +1892,7 @@ export class ScimGateway {
1889
1892
  const id = ctx.routeObj.id ? decodeURIComponent(ctx.routeObj.id) : ctx.routeObj.id
1890
1893
  const obj = ctx.request.body
1891
1894
 
1892
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [PUT ${handle[0].toUpperCase() + handle.slice(1)}] id=${id} body=${JSON.stringify(obj)}`)
1895
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [PUT ${handle[0].toUpperCase() + handle.slice(1)}] id=${id} body=${JSON.stringify(obj)}`, { baseEntity: ctx?.routeObj?.baseEntity })
1893
1896
  try {
1894
1897
  if (!obj) throw new Error('missing body')
1895
1898
  if (typeof obj !== 'object') throw new Error('body is not JSON')
@@ -1946,7 +1949,7 @@ export class ScimGateway {
1946
1949
 
1947
1950
  const postBulkHandler = async (ctx: Context) => {
1948
1951
  const baseEntity = ctx.routeObj.baseEntity
1949
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Bulk Operations]`)
1952
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [Bulk Operations]`, { baseEntity: ctx?.routeObj?.baseEntity })
1950
1953
  const bulkBody: SCIMBulkRequest = utils.copyObj(ctx.request.body)
1951
1954
  try {
1952
1955
  if (!bulkBody) throw new Error('missing body')
@@ -2098,7 +2101,7 @@ export class ScimGateway {
2098
2101
  const postApiHandler = async (ctx: Context) => {
2099
2102
  const baseEntity = ctx.routeObj.baseEntity
2100
2103
  const obj = ctx.request.body
2101
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [POST ${ctx.routeObj.handle}]`)
2104
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [POST ${ctx.routeObj.handle}]`, { baseEntity: ctx?.routeObj?.baseEntity })
2102
2105
 
2103
2106
  if (!obj) {
2104
2107
  const err = new Error('missing body')
@@ -2107,7 +2110,7 @@ export class ScimGateway {
2107
2110
  return
2108
2111
  }
2109
2112
  try {
2110
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling postApi and awaiting result`)
2113
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling postApi and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
2111
2114
  let result = await this.postApi(baseEntity, obj, ctx.passThrough)
2112
2115
  if (result) {
2113
2116
  if (typeof result === 'object') result = { result: result }
@@ -2147,7 +2150,7 @@ export class ScimGateway {
2147
2150
  const baseEntity = ctx.routeObj.baseEntity
2148
2151
  const id = ctx.routeObj.id
2149
2152
  const obj = ctx.request.body
2150
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [PUT ${ctx.routeObj.handle}] id=${id}`)
2153
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [PUT ${ctx.routeObj.handle}] id=${id}`, { baseEntity: ctx?.routeObj?.baseEntity })
2151
2154
 
2152
2155
  try {
2153
2156
  if (!obj) throw new Error('missing body')
@@ -2159,7 +2162,7 @@ export class ScimGateway {
2159
2162
  }
2160
2163
 
2161
2164
  try {
2162
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling putApi and awaiting result`)
2165
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling putApi and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
2163
2166
  let result = await this.putApi(baseEntity, id, obj, ctx.passThrough)
2164
2167
  if (result) {
2165
2168
  if (typeof result === 'object') result = { result }
@@ -2200,7 +2203,7 @@ export class ScimGateway {
2200
2203
  const id = ctx.routeObj.id as string
2201
2204
  const body = ctx.request.body
2202
2205
 
2203
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [PATCH ${handle} ] id=${id}`)
2206
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [PATCH ${handle} ] id=${id}`, { baseEntity: ctx?.routeObj?.baseEntity })
2204
2207
 
2205
2208
  if (!body) {
2206
2209
  const err = new Error('missing body')
@@ -2209,7 +2212,7 @@ export class ScimGateway {
2209
2212
  return
2210
2213
  } else {
2211
2214
  try {
2212
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling patchApi and awaiting result`)
2215
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling patchApi and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
2213
2216
  let result = await this.patchApi(baseEntity, id, body, ctx.passThrough)
2214
2217
  if (result) {
2215
2218
  if (typeof result === 'object') result = { result }
@@ -2250,11 +2253,11 @@ export class ScimGateway {
2250
2253
  const id = ctx.routeObj.id as string
2251
2254
  const body = ctx.request.body
2252
2255
 
2253
- if (id) logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [GET ${handle}] id=${id}`)
2256
+ if (id) logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [GET ${handle}] id=${id}`, { baseEntity: ctx?.routeObj?.baseEntity })
2254
2257
  else logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [GET ${handle}]`)
2255
2258
 
2256
2259
  try {
2257
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling getApi and awaiting result`)
2260
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling getApi and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
2258
2261
  let result = await this.getApi(baseEntity, id, ctx.query, body, ctx.passThrough)
2259
2262
  if (result) {
2260
2263
  if (typeof result === 'object') result = { result }
@@ -2289,10 +2292,10 @@ export class ScimGateway {
2289
2292
  const deleteApiHandler = async (ctx: Context) => {
2290
2293
  const baseEntity = ctx.routeObj.baseEntity
2291
2294
  const id = ctx.routeObj.id
2292
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [DELETE ${ctx.routeObj.handle} ] id=${id}`)
2295
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [DELETE ${ctx.routeObj.handle} ] id=${id}`, { baseEntity: ctx?.routeObj?.baseEntity })
2293
2296
  try {
2294
2297
  if (!id || id.includes('/')) throw new Error('missing id')
2295
- logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling deleteApi and awaiting result`)
2298
+ logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] calling deleteApi and awaiting result`, { baseEntity: ctx?.routeObj?.baseEntity })
2296
2299
  let result = await this.deleteApi(baseEntity, id, ctx.passThrough)
2297
2300
  if (result) {
2298
2301
  if (typeof result === 'object') result = { result: result }
@@ -2334,7 +2337,7 @@ export class ScimGateway {
2334
2337
  try {
2335
2338
  const ob = { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }
2336
2339
  const attributes = ['id', 'displayName']
2337
- logger.debug(`${gwName}[${pluginName}][${baseEntity}] calling ${handler.groups.getMethod} and awaiting result - groups to be included`)
2340
+ logger.debug(`${gwName}[${pluginName}][${baseEntity}] calling ${handler.groups.getMethod} and awaiting result - groups to be included`, { baseEntity })
2338
2341
  res = await (this as any)[handler.groups.getMethod](baseEntity, ob, attributes, ctxPassThrough)
2339
2342
  } catch (err) { void 0 }
2340
2343
  if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
@@ -2550,6 +2553,7 @@ export class ScimGateway {
2550
2553
  } else if (!ctx.routeObj.handle) {
2551
2554
  ctx.response.status = 404 // NOT_FOUND
2552
2555
  } else if (!ipAllowList(ctx.ip)) {
2556
+ logger.debug(`${gwName}[${pluginName}] client ip ${ctx.ip} not in ipAllowList`, { baseEntity: ctx?.routeObj?.baseEntity })
2553
2557
  ctx.response.status = 401
2554
2558
  } else if (!await isAuthorized(ctx)) {
2555
2559
  ctx.response.status = 401
@@ -2565,14 +2569,14 @@ export class ScimGateway {
2565
2569
  const chainingBaseUrl = this.config.scimgateway.chainingBaseUrl // http(s)://<host>:<port>
2566
2570
  if (!chainingBaseUrl) {
2567
2571
  ctx.response.status = 500
2568
- logger.error(`${gwName}[${pluginName}] onChainingHandler error: configuration scimgateway.chainingBaseUrl missing`)
2572
+ logger.error(`${gwName}[${pluginName}] onChainingHandler error: configuration scimgateway.chainingBaseUrl missing`, { baseEntity: ctx?.routeObj?.baseEntity })
2569
2573
  return
2570
2574
  }
2571
2575
  try {
2572
2576
  new URL(chainingBaseUrl)
2573
2577
  } catch (err: any) {
2574
2578
  ctx.response.status = 500
2575
- logger.error(`${gwName}[${pluginName}] onChainingHandler error: configuration scimgateway.chainingBaseUrl must use correct syntax 'http(s)://host:port' error: ${err.message}`)
2579
+ logger.error(`${gwName}[${pluginName}] onChainingHandler error: configuration scimgateway.chainingBaseUrl must use correct syntax 'http(s)://host:port' error: ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
2576
2580
  return
2577
2581
  }
2578
2582
  try {
@@ -2597,7 +2601,7 @@ export class ScimGateway {
2597
2601
  ctx.response.body = jBody.body ? JSON.stringify(jBody.body) : err.message
2598
2602
  } catch (parseErr) {
2599
2603
  ctx.response.status = 500
2600
- logger.error(`${gwName}[${pluginName}] onChainingHandler error: ${err.message}`)
2604
+ logger.error(`${gwName}[${pluginName}] onChainingHandler error: ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
2601
2605
  }
2602
2606
  }
2603
2607
  }
@@ -2605,14 +2609,14 @@ export class ScimGateway {
2605
2609
  const onPublisherHandler = async (ctx: Context) => {
2606
2610
  if (!this.pub) {
2607
2611
  ctx.response.status = 500
2608
- logger.error(`${gwName}[${pluginName}] onPublisherHandler error: publisher not initialized`)
2612
+ logger.error(`${gwName}[${pluginName}] onPublisherHandler error: publisher not initialized`, { baseEntity: ctx?.routeObj?.baseEntity })
2609
2613
  return
2610
2614
  }
2611
2615
  try {
2612
2616
  ctx.response = await this.pub.publish({ ctx })
2613
2617
  } catch (err: any) {
2614
2618
  ctx.response.status = 500
2615
- logger.error(`${gwName}[${pluginName}] onPublisherHandler error: ${err.message}`)
2619
+ logger.error(`${gwName}[${pluginName}] onPublisherHandler error: ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
2616
2620
  return
2617
2621
  }
2618
2622
  }
@@ -2702,15 +2706,39 @@ export class ScimGateway {
2702
2706
  const isPublisherEnabled = this.config.scimgateway.stream.publisher.enabled
2703
2707
  const isChainingEnabled = this.config.scimgateway.chainingBaseUrl
2704
2708
 
2705
- const wssInit = `
2709
+ const sseInit = `
2706
2710
  <!DOCTYPE html>
2707
2711
  <html>
2708
2712
  <head>
2709
2713
  <style>
2714
+ html, body {
2715
+ height: 100%;
2716
+ margin: 0;
2717
+ padding: 0;
2718
+ }
2719
+ body {
2720
+ display: flex;
2721
+ flex-direction: column;
2722
+ height: 100vh;
2723
+ margin-left: 8px;
2724
+ }
2710
2725
  .header-flex {
2711
2726
  display: flex;
2712
2727
  align-items: center;
2713
2728
  gap: 16px;
2729
+ flex-shrink: 0;
2730
+ margin-top: 2px;
2731
+ margin-bottom: 2px;
2732
+ }
2733
+ #log {
2734
+ flex: 1 1 auto;
2735
+ width: 100%;
2736
+ overflow: auto;
2737
+ white-space: pre;
2738
+ margin: 0;
2739
+ min-height: 0;
2740
+ height: auto;
2741
+ box-sizing: border-box;
2714
2742
  }
2715
2743
  #stopBtn {
2716
2744
  padding: 4px 18px;
@@ -2725,42 +2753,41 @@ export class ScimGateway {
2725
2753
  </head>
2726
2754
  <body>
2727
2755
  <div class="header-flex">
2728
- <h3 style="margin:0;">SCIM Gateway remote logger</h3>
2756
+ <h3>SCIM Gateway remote logger</h3>
2729
2757
  <button id="stopBtn" type="button">Stop</button>
2730
2758
  </div>
2731
2759
  <pre id="log"></pre>
2732
2760
  <script>
2733
2761
  const stopBtn = document.getElementById('stopBtn')
2734
2762
  const logElem = document.getElementById('log')
2735
- let ws = new WebSocket('{{protocol}}//' + location.host + '/logger')
2736
- ws.onmessage = function(event) {
2737
- event.data.split('\\n').forEach(function(line) {
2738
- if (!line.trim()) return
2739
- const htmlLine = line.replace(
2740
- /(level":"\\s*)(debug|info|warn|error)/i,
2741
- function(match, p1, p2) {
2742
- let color = ''
2743
- switch (p2.toLowerCase()) {
2744
- case 'debug': color = '#888'; break
2745
- case 'info': color = 'blue'; break
2746
- case 'warn': color = 'orange'; break
2747
- case 'error': color = 'red'; break
2748
- default: color = 'black'
2749
- }
2750
- return p1 + '<span style="color:' + color + ';font-weight:bold">' + p2 + '</span>'
2763
+ let es = new EventSource(location.pathname)
2764
+
2765
+ es.onmessage = function(event) {
2766
+ if (!event.data.trim()) return
2767
+ const htmlLine = event.data.replace(
2768
+ /(level":"\s*)(debug|info|warn|error)/i,
2769
+ function(match, p1, p2) {
2770
+ let color = ''
2771
+ switch (p2.toLowerCase()) {
2772
+ case 'debug': color = '#888'; break
2773
+ case 'info': color = 'blue'; break
2774
+ case 'warn': color = 'orange'; break
2775
+ case 'error': color = 'red'; break
2776
+ default: color = 'black'
2751
2777
  }
2752
- );
2753
- logElem.innerHTML += htmlLine + '<br>'
2754
- })
2778
+ return p1 + '<span style="color:' + color + ';font-weight:bold">' + p2 + '</span>'
2779
+ }
2780
+ )
2781
+ logElem.innerHTML += htmlLine + '<br>'
2782
+ logElem.scrollTop = logElem.scrollHeight
2755
2783
  }
2784
+
2756
2785
  stopBtn.onclick = function() {
2757
- if (ws) {
2758
- ws.close()
2759
- ws = null
2786
+ if (es) {
2787
+ es.close()
2788
+ es = null
2760
2789
  stopBtn.textContent = 'Start'
2761
- stopBtn.onclick = function() {
2762
- location.reload()
2763
- }
2790
+ stopBtn.onclick = function() { location.reload() }
2764
2791
  }
2765
2792
  }
2766
2793
  </script>
@@ -2804,28 +2831,18 @@ export class ScimGateway {
2804
2831
  await getHandlerServiceProviderConfig(ctx)
2805
2832
  return await onAfterHandle(ctx)
2806
2833
  case 'GET logger': // no onAfterHandle
2807
- if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') { // browser step 2, and other Bun ws(s) clients
2808
- logger.info(`${gwName}[${pluginName}] remote logger connected from ip address ${ctx.ip}`)
2809
- return server.upgrade(req, { // after upgrade, the server will handle the WebSocket connection configured in Bun.serve()
2810
- data: { // passed to WebSocket server Bun open handler
2811
- headers: req.headers,
2812
- url: req.url,
2813
- ip: ctx.ip,
2814
- nonce: this.Nonce.createItem(crypto.randomUUID()),
2815
- },
2816
- })
2817
- }
2818
- if (req.headers.has('sec-fetch-dest') && typeof Bun !== 'undefined') { // client is browser and not supporting WebSocket on Node.js
2819
- const url = new URL(ctx.origin)
2820
- const protocol = (url.protocol === 'https:' ? 'wss:' : 'ws:')
2821
- const js = wssInit.replace('{{protocol}}', protocol)
2822
- return new Response(js, { // browser step 1 => force WebSocket by sending javascript
2823
- status: 200,
2824
- headers: {
2825
- 'Content-Type': 'text/html; charset=utf-8',
2826
- },
2827
- })
2828
- } else return await getHandlerLoggerSSE(ctx) // using SSE for none WebSocket/wss e.g. curl -Ns http://localhost:8880/logger -u gwadmin:password | sed 's/\xE2\x80\x8B//g'
2834
+ if (req.headers.has('sec-fetch-dest')) { // client is browser
2835
+ if (ctx.request.headers.get('accept')?.includes('text/event-stream')) {
2836
+ return await getHandlerLoggerSSE(ctx)
2837
+ } else {
2838
+ return new Response(sseInit, {
2839
+ status: 200,
2840
+ headers: {
2841
+ 'Content-Type': 'text/html; charset=utf-8',
2842
+ },
2843
+ })
2844
+ }
2845
+ } else return await getHandlerLoggerSSE(ctx)
2829
2846
  case 'PATCH users':
2830
2847
  case 'PATCH groups':
2831
2848
  await patchHandler(ctx)
@@ -2880,44 +2897,9 @@ export class ScimGateway {
2880
2897
  const reqWithRaw = req as Request & { raw: IncomingMessage }
2881
2898
  return await route(reqWithRaw, srv.requestIP(req)?.address || '')
2882
2899
  },
2883
- websocket: {
2884
- open: (ws) => {
2885
- const data = ws.data as { headers: Headers, url: string, ip: string, nonce: string } || {}
2886
- let isAuthorized = false // client is already authenticated by initial http/https upgrade to websocket, anyhow passing data to be validated
2887
- if (data?.nonce && this.Nonce.isItemValid(data.nonce)) {
2888
- if (data.headers.has('authorization')) {
2889
- if (data.url.endsWith('/logger')) isAuthorized = true
2890
- }
2891
- }
2892
- if (!isAuthorized) {
2893
- logger.error(`${gwName}[${pluginName}] remote logger ip address ${data.ip} - WebSocket connection error: invalid nonce`)
2894
- ws.close(3000, 'Unauthorized')
2895
- return
2896
- }
2897
-
2898
- const levelInt = logger.levelToInt(this.config?.scimgateway?.log?.loglevel?.push || 'info')
2899
- const sub = async (msgObj: Record<string, any>) => {
2900
- if (logger.levelToInt(msgObj.level) < levelInt) return
2901
- ws.send(`${JSON.stringify(msgObj)}`)
2902
- }
2903
- logger.subscribe(sub)
2904
- ;(ws as any)._sub = sub
2905
- ;(ws as any)._ip = data.ip
2906
- },
2907
- close: (ws) => {
2908
- const sub = (ws as any)._sub
2909
- const ip = (ws as any)._ip
2910
- if (sub) {
2911
- logger.unsubscribe(sub)
2912
- }
2913
- logger.info(`${gwName}[${pluginName}] remote logger disconnected from ip address ${ip}`)
2914
- },
2915
- message: () => {},
2916
- },
2917
2900
  })
2918
2901
  } else {
2919
2902
  // using nodejs server either through Bun compability or Node.js
2920
-
2921
2903
  // get body from req
2922
2904
  async function getRequestBody(req: any): Promise<Buffer> {
2923
2905
  return new Promise((resolve, reject) => {
@@ -3185,28 +3167,28 @@ export class ScimGateway {
3185
3167
  * logDebug logs debug message
3186
3168
  **/
3187
3169
  logDebug(baseEntity: string | undefined, msg: string) {
3188
- this.logger.debug(`${this.pluginName}[${baseEntity}] ${msg}`)
3170
+ this.logger.debug(`${this.pluginName}[${baseEntity}] ${msg}`, { baseEntity })
3189
3171
  }
3190
3172
 
3191
3173
  /**
3192
3174
  * logInfo logs info message
3193
3175
  **/
3194
3176
  logInfo(baseEntity: string | undefined, msg: string) {
3195
- this.logger.info(`${this.pluginName}[${baseEntity}] ${msg}`)
3177
+ this.logger.info(`${this.pluginName}[${baseEntity}] ${msg}`, { baseEntity })
3196
3178
  }
3197
3179
 
3198
3180
  /**
3199
3181
  * logWarn logs warning message
3200
3182
  **/
3201
3183
  logWarn(baseEntity: string | undefined, msg: string) {
3202
- this.logger.warn(`${this.pluginName}[${baseEntity}] ${msg}`)
3184
+ this.logger.warn(`${this.pluginName}[${baseEntity}] ${msg}`, { baseEntity })
3203
3185
  }
3204
3186
 
3205
3187
  /**
3206
3188
  * logError logs error message
3207
3189
  **/
3208
3190
  logError(baseEntity: string | undefined, msg: string) {
3209
- this.logger.error(`${this.pluginName}[${baseEntity}] ${msg}`)
3191
+ this.logger.error(`${this.pluginName}[${baseEntity}] ${msg}`, { baseEntity })
3210
3192
  }
3211
3193
 
3212
3194
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.4.1",
3
+ "version": "5.4.3",
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)",