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 +72 -58
- package/lib/helper-rest.ts +1 -1
- package/lib/logger.ts +11 -10
- package/lib/scimgateway.ts +127 -145
- package/package.json +1 -1
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** -
|
|
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
|
-
-
|
|
750
|
-
- curl -
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
//
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
headers
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
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]
|
package/lib/helper-rest.ts
CHANGED
|
@@ -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(
|
|
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
|
/**
|
package/lib/scimgateway.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
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
|
-
|
|
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 (
|
|
2758
|
-
|
|
2759
|
-
|
|
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.
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
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