@continuoussecuritytooling/keycloak-reporter 0.1.0

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.
Files changed (45) hide show
  1. package/.bin/start-server.mjs +87 -0
  2. package/.bin/wait-for-server.sh +13 -0
  3. package/.docs/webhook-slack-sample.png +0 -0
  4. package/.docs/webhook-teams-sample.png +0 -0
  5. package/.editorconfig +23 -0
  6. package/.eslintignore +1 -0
  7. package/.eslintrc.cjs +14 -0
  8. package/.github/FUNDING.yml +2 -0
  9. package/.github/workflows/pipeline.yml +101 -0
  10. package/Dockerfile +9 -0
  11. package/LICENSE +21 -0
  12. package/README.md +43 -0
  13. package/cli.ts +125 -0
  14. package/dist/cli.js +83 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/index.js +83 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/client.js +41 -0
  19. package/dist/lib/client.js.map +1 -0
  20. package/dist/lib/convert.js +9 -0
  21. package/dist/lib/convert.js.map +1 -0
  22. package/dist/lib/output.js +98 -0
  23. package/dist/lib/output.js.map +1 -0
  24. package/dist/lib/user.js +75 -0
  25. package/dist/lib/user.js.map +1 -0
  26. package/dist/package.json +53 -0
  27. package/dist/src/cli.js +19 -0
  28. package/dist/src/cli.js.map +1 -0
  29. package/docker_entrypoint.sh +3 -0
  30. package/e2e/fixtures/auth-utils/test-realm.json +5095 -0
  31. package/e2e/run-tests.sh +45 -0
  32. package/e2e/spec/clients.js +29 -0
  33. package/e2e/spec/users.js +29 -0
  34. package/e2e/spec/webhooks.js +60 -0
  35. package/index.ts +125 -0
  36. package/jest.config.js +25 -0
  37. package/lib/client.ts +55 -0
  38. package/lib/convert.ts +9 -0
  39. package/lib/output.ts +115 -0
  40. package/lib/user.ts +99 -0
  41. package/package.json +53 -0
  42. package/renovate.json +14 -0
  43. package/src/cli.ts +26 -0
  44. package/test/convert.spec.ts +15 -0
  45. package/tsconfig.json +11 -0
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ import { Octokit } from '@octokit/rest'
4
+ import gunzip from 'gunzip-maybe'
5
+ import fetch from 'node-fetch'
6
+ import { spawn } from 'node:child_process'
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { pipeline } from 'node:stream'
10
+ import { fileURLToPath } from 'node:url'
11
+ import { promisify } from 'node:util'
12
+ import tar from 'tar-fs'
13
+
14
+ const DIR_NAME = path.dirname(fileURLToPath(import.meta.url))
15
+ const SERVER_DIR = path.resolve(DIR_NAME, '../tmp/server')
16
+ const SCRIPT_EXTENSION = process.platform === 'win32' ? '.bat' : '.sh'
17
+
18
+ // TODO: Once support for Node.js 14 has been dropped this can be replaced with an import from 'node:stream/promises'.
19
+ // More information: https://nodejs.org/api/stream.html#streams-promises-api
20
+ const pipelineAsync = promisify(pipeline)
21
+
22
+ await startServer()
23
+
24
+ async function startServer () {
25
+ await downloadServer()
26
+
27
+ console.info('Starting server…')
28
+
29
+ const args = process.argv.slice(2)
30
+ const child = spawn(
31
+ path.join(SERVER_DIR, `bin/kc${SCRIPT_EXTENSION}`),
32
+ ['start-dev', ...args],
33
+ {
34
+ env: {
35
+ KEYCLOAK_ADMIN: 'admin',
36
+ KEYCLOAK_ADMIN_PASSWORD: 'admin',
37
+ ...process.env
38
+ }
39
+ }
40
+ )
41
+
42
+ child.stdout.pipe(process.stdout)
43
+ child.stderr.pipe(process.stderr)
44
+ }
45
+
46
+ async function downloadServer () {
47
+ const directoryExists = fs.existsSync(SERVER_DIR)
48
+
49
+ if (directoryExists) {
50
+ console.info('Server installation found, skipping download.')
51
+ return
52
+ }
53
+
54
+ console.info('Downloading and extracting server…')
55
+
56
+ const nightlyAsset = await getNightlyAsset()
57
+ const assetStream = await getAssetAsStream(nightlyAsset)
58
+
59
+ await extractTarball(assetStream, SERVER_DIR, { strip: 1 })
60
+ }
61
+
62
+ async function getNightlyAsset () {
63
+ const api = new Octokit()
64
+ const release = await api.repos.getReleaseByTag({
65
+ owner: 'keycloak',
66
+ repo: 'keycloak',
67
+ tag: 'nightly'
68
+ })
69
+
70
+ return release.data.assets.find(
71
+ ({ name }) => name === 'keycloak-999.0.0-SNAPSHOT.tar.gz'
72
+ )
73
+ }
74
+
75
+ async function getAssetAsStream (asset) {
76
+ const response = await fetch(asset.browser_download_url)
77
+
78
+ if (!response.ok) {
79
+ throw new Error('Something went wrong requesting the nightly release.')
80
+ }
81
+
82
+ return response.body
83
+ }
84
+
85
+ function extractTarball (stream, path, options) {
86
+ return pipelineAsync(stream, gunzip(), tar.extract(path, options))
87
+ }
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+ # For debugging purposes to make sure the image was started
3
+ counter=0
4
+ printf 'Waiting for Keycloak server to start'
5
+ until $(curl --output /dev/null --silent --head --fail http://localhost:8080/realms/master/.well-known/openid-configuration); do
6
+ printf '.'
7
+ sleep 5
8
+ if [[ "$counter" -gt 24 ]]; then
9
+ printf "Keycloak server failed to start. Timeout!"
10
+ exit 1
11
+ fi
12
+ counter=$((counter + 1))
13
+ done
Binary file
Binary file
package/.editorconfig ADDED
@@ -0,0 +1,23 @@
1
+ # EditorConfig helps developers define and maintain consistent
2
+ # coding styles between different editors and IDEs
3
+ # editorconfig.org
4
+
5
+ root = true
6
+
7
+ [*]
8
+
9
+ # Change these settings to your own preference
10
+ indent_style = space
11
+ indent_size = 2
12
+
13
+ # We recommend you to keep these unchanged
14
+ end_of_line = lf
15
+ charset = utf-8
16
+ trim_trailing_whitespace = true
17
+ insert_final_newline = true
18
+
19
+ [*.md]
20
+ trim_trailing_whitespace = false
21
+
22
+ [*.ts,*.js]
23
+ quote_type = single
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ dist/**
package/.eslintrc.cjs ADDED
@@ -0,0 +1,14 @@
1
+ /* eslint-env node */
2
+ module.exports = {
3
+ env: {
4
+ node: true,
5
+ commonjs: true,
6
+ },
7
+ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
8
+ parser: '@typescript-eslint/parser',
9
+ plugins: ['@typescript-eslint'],
10
+ root: true,
11
+ rules: {
12
+ quotes: [2, 'single', { avoidEscape: true }],
13
+ },
14
+ };
@@ -0,0 +1,2 @@
1
+ # These are supported funding model platforms
2
+ open_collective: m13t
@@ -0,0 +1,101 @@
1
+ name: Build
2
+ "on":
3
+ - push
4
+ jobs:
5
+ build:
6
+ name: "Build and Test on Node ${{ matrix.node_version }} and ${{ matrix.os }}"
7
+ runs-on: "${{ matrix.os }}"
8
+ strategy:
9
+ matrix:
10
+ node_version:
11
+ - 16
12
+ - 18
13
+ - 20
14
+ os:
15
+ - ubuntu-latest
16
+ - macOS-latest
17
+ - windows-latest
18
+ steps:
19
+ - uses: actions/checkout@v3
20
+ - name: "Use Node.js ${{ matrix.node_version }}"
21
+ uses: actions/setup-node@v3
22
+ with:
23
+ node-version: "${{ matrix.node_version }}"
24
+ - name: npm build and test
25
+ run: |
26
+ npm run clean
27
+ npm run build
28
+ npm run test
29
+
30
+ - uses: actions/upload-artifact@v3
31
+ with:
32
+ name: dist-folder
33
+ path: dist
34
+
35
+ end2end:
36
+ name: "End2End Test on Node ${{ matrix.node_version }} and ${{ matrix.os }}"
37
+ runs-on: "${{ matrix.os }}"
38
+ needs:
39
+ - build
40
+ strategy:
41
+ matrix:
42
+ node_version:
43
+ - 16
44
+ - 18
45
+ - 20
46
+ os:
47
+ - ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v3
50
+ - name: "Use Node.js ${{ matrix.node_version }}"
51
+ uses: actions/setup-node@v3
52
+ with:
53
+ node-version: "${{ matrix.node_version }}"
54
+ - name: npm build and test
55
+ run: |
56
+ npm run clean
57
+ npm run build
58
+
59
+ - name: Start Keycloak server
60
+ run: npm run end2end:start-server &
61
+
62
+ - name: Wait for Keycloak server
63
+ run: .bin/wait-for-server.sh
64
+
65
+ - name: Run end2end tests
66
+ run: npm run end2end:test
67
+ env:
68
+ WEBHOOK_TESTING_TEAMS: ${{ secrets.WEBHOOK_TESTING_TEAMS }}
69
+ WEBHOOK_TESTING_SLACK: ${{ secrets.WEBHOOK_TESTING_SLACK }}
70
+
71
+ package:
72
+ name: Build Container Image
73
+ runs-on: ubuntu-latest
74
+ needs:
75
+ - build
76
+ - end2end
77
+ steps:
78
+ - uses: actions/checkout@v3
79
+ - uses: actions/setup-node@v3
80
+ - name: "Build Package"
81
+ run: |
82
+ npm run clean
83
+ npm run build
84
+ - name: Buildah Action
85
+ id: build-image
86
+ uses: redhat-actions/buildah-build@v2
87
+ with:
88
+ image: continuoussecuritytooling/keycloak-reporting-cli
89
+ tags: "v1 ${{ github.sha }}"
90
+ containerfiles: |
91
+ ./Dockerfile
92
+ - name: Push To Docker Hub
93
+ id: push-to-dockerhub
94
+ uses: redhat-actions/push-to-registry@v2
95
+ with:
96
+ image: ${{ steps.build-image.outputs.image }}
97
+ tags: ${{ steps.build-image.outputs.tags }}
98
+ registry: registry.hub.docker.com
99
+ username: continuoussecuritytooling
100
+ password: ${{ secrets.DOCKER_HUB_TOKEN }}
101
+ if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main'
package/Dockerfile ADDED
@@ -0,0 +1,9 @@
1
+ FROM node:20
2
+
3
+ COPY dist/ docker_entrypoint.sh /app
4
+
5
+ WORKDIR /app
6
+
7
+ RUN cd /app && npm i
8
+
9
+ ENTRYPOINT ["/app/docker_entrypoint.sh"]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Continuous Security Tools
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Keycloak Reporter
2
+
3
+ [![Docker Stars](https://img.shields.io/docker/stars/continuoussecuritytooling/keycloak-reporting-cli.svg)](https://hub.docker.com/r/continuoussecuritytooling/keycloak-reporting-cli/)
4
+
5
+ ## Usage
6
+
7
+ ```
8
+ docker run continuoussecuritytooling/keycloak-reporting-cli listClients <Keycloak_Root_URL> <Client_ID> <Client_Secret> --format=csv
9
+ ```
10
+
11
+ The output looks for CSV, like that:
12
+ ```
13
+ "client","description","realm","enabled","public","allowedOrigins"
14
+ "account",,"bunge",true,true,"[]"
15
+ "account-console",,"bunge",true,true,"[]"
16
+ "admin-cli",,"bunge",true,true,"[]"
17
+ "broker",,"bunge",true,false,"[]"
18
+ "portal",,"bunge",true,false,"[]"
19
+ "realm-management",,"bunge",true,false,"[]"
20
+ "security-admin-console",,"bunge",true,true,"[""+""]"
21
+ ```
22
+
23
+ Valid commands are:
24
+ - `listClients`
25
+ - `listUsers`
26
+
27
+ ## Advanced
28
+
29
+ ### Post to Slack or Teams
30
+
31
+ When using this command:
32
+ ```
33
+ node dist/index.js listUsers <Keycloak_Root_URL> <Client_ID> <Client_Secret> --format=json --output=webhook --webhookType=slack --webhookUrl=$WEBHOOK_TESTING_SLACK
34
+ ```
35
+ the following entry in slack will be created:
36
+ ![Slack Sample](.docs/webhook-slack-sample.png)
37
+
38
+ And for Teams:
39
+ ```
40
+ node dist/index.js listUsers <Keycloak_Root_URL> <Client_ID> <Client_Secret> --format=json --output=webhook --webhookType=teams --webhookUrl=$WEBHOOK_TESTING_TEAMS
41
+ ```
42
+ the following entry in slack will be created:
43
+ ![Team Sample](.docs/webhook-teams-sample.png)
package/cli.ts ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from 'yargs/yargs';
4
+ import { hideBin } from 'yargs/helpers';
5
+ import { listUsers, listClients } from './src/cli.js';
6
+ import { Options } from './lib/client.js';
7
+ import { convertJSON2CSV } from './lib/convert.js';
8
+ import { post2Webhook } from './lib/output.js';
9
+
10
+ class WebhookConfig {
11
+ type: string;
12
+ url: string;
13
+ title: string;
14
+ constructor(type: string, url: string, title: string) {
15
+ this.type = type;
16
+ this.url = url;
17
+ this.title = title;
18
+ }
19
+ }
20
+
21
+ async function convert(
22
+ format: string,
23
+ output: string,
24
+ config: WebhookConfig,
25
+ json: object
26
+ ) {
27
+ let outputContent: string;
28
+ switch (format) {
29
+ case 'csv':
30
+ outputContent = (await convertJSON2CSV(json)).toString();
31
+ break;
32
+ // defaulting to JSON
33
+ default:
34
+ outputContent = JSON.stringify(json);
35
+ }
36
+ switch (output) {
37
+ case 'webhook':
38
+ try {
39
+ await post2Webhook(
40
+ config.type,
41
+ config.url,
42
+ config.title,
43
+ outputContent
44
+ );
45
+ } catch (e) {
46
+ console.error('Error during sending webhook: ', e);
47
+ }
48
+ break;
49
+ // defaulting to standard out
50
+ default:
51
+ console.log(outputContent);
52
+ }
53
+ }
54
+
55
+ yargs(hideBin(process.argv))
56
+ .command(
57
+ 'listUsers [url] [clientId] [clientSecret]',
58
+ 'fetches all users in the realms.',
59
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
60
+ () => {},
61
+ async (argv) => {
62
+ const users = await listUsers(<Options>{
63
+ clientId: argv.clientId as string,
64
+ clientSecret: argv.clientSecret as string,
65
+ rootUrl: argv.url as string,
66
+ });
67
+ await convert(
68
+ argv.format as string,
69
+ argv.output as string,
70
+ new WebhookConfig(
71
+ argv.webhookType as string,
72
+ argv.webhookUrl as string,
73
+ 'User Listing'
74
+ ),
75
+ users
76
+ );
77
+ }
78
+ )
79
+ .command(
80
+ 'listClients [url] [clientId] [clientSecret]',
81
+ 'fetches all clients in the realms.',
82
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
83
+ () => {},
84
+ async (argv) => {
85
+ const clients = await listClients(<Options>{
86
+ clientId: argv.clientId as string,
87
+ clientSecret: argv.clientSecret as string,
88
+ rootUrl: argv.url as string,
89
+ });
90
+ await convert(
91
+ argv.format as string,
92
+ argv.output as string,
93
+ new WebhookConfig(
94
+ argv.webhookType as string,
95
+ argv.webhookUrl as string,
96
+ 'Client Listing'
97
+ ),
98
+ clients
99
+ );
100
+ }
101
+ )
102
+ .option('format', {
103
+ alias: 'f',
104
+ type: 'string',
105
+ default: 'json',
106
+ description: 'output format, e.g. JSON|CSV',
107
+ })
108
+ .option('output', {
109
+ alias: 'o',
110
+ type: 'string',
111
+ default: 'stdout',
112
+ description: 'output channel',
113
+ })
114
+ .option('webhookType', {
115
+ alias: 'w',
116
+ type: 'string',
117
+ default: 'slack',
118
+ description: 'Webhook Type',
119
+ })
120
+ .option('webhookUrl', {
121
+ alias: 't',
122
+ type: 'string',
123
+ description: 'Webhook URL',
124
+ })
125
+ .parse();
package/dist/cli.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import yargs from 'yargs/yargs';
3
+ import { hideBin } from 'yargs/helpers';
4
+ import { listUsers, listClients } from './src/cli.js';
5
+ import { convertJSON2CSV } from './lib/convert.js';
6
+ import { post2Webhook } from './lib/output.js';
7
+ class WebhookConfig {
8
+ constructor(type, url, title) {
9
+ this.type = type;
10
+ this.url = url;
11
+ this.title = title;
12
+ }
13
+ }
14
+ async function convert(format, output, config, json) {
15
+ let outputContent;
16
+ switch (format) {
17
+ case 'csv':
18
+ outputContent = (await convertJSON2CSV(json)).toString();
19
+ break;
20
+ // defaulting to JSON
21
+ default:
22
+ outputContent = JSON.stringify(json);
23
+ }
24
+ switch (output) {
25
+ case 'webhook':
26
+ try {
27
+ await post2Webhook(config.type, config.url, config.title, outputContent);
28
+ }
29
+ catch (e) {
30
+ console.error('Error during sending webhook: ', e);
31
+ }
32
+ break;
33
+ // defaulting to standard out
34
+ default:
35
+ console.log(outputContent);
36
+ }
37
+ }
38
+ yargs(hideBin(process.argv))
39
+ .command('listUsers [url] [clientId] [clientSecret]', 'fetches all users in the realms.',
40
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
41
+ () => { }, async (argv) => {
42
+ const users = await listUsers({
43
+ clientId: argv.clientId,
44
+ clientSecret: argv.clientSecret,
45
+ rootUrl: argv.url,
46
+ });
47
+ await convert(argv.format, argv.output, new WebhookConfig(argv.webhookType, argv.webhookUrl, 'User Listing'), users);
48
+ })
49
+ .command('listClients [url] [clientId] [clientSecret]', 'fetches all clients in the realms.',
50
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
51
+ () => { }, async (argv) => {
52
+ const clients = await listClients({
53
+ clientId: argv.clientId,
54
+ clientSecret: argv.clientSecret,
55
+ rootUrl: argv.url,
56
+ });
57
+ await convert(argv.format, argv.output, new WebhookConfig(argv.webhookType, argv.webhookUrl, 'Client Listing'), clients);
58
+ })
59
+ .option('format', {
60
+ alias: 'f',
61
+ type: 'string',
62
+ default: 'json',
63
+ description: 'output format, e.g. JSON|CSV',
64
+ })
65
+ .option('output', {
66
+ alias: 'o',
67
+ type: 'string',
68
+ default: 'stdout',
69
+ description: 'output channel',
70
+ })
71
+ .option('webhookType', {
72
+ alias: 'w',
73
+ type: 'string',
74
+ default: 'slack',
75
+ description: 'Webhook Type',
76
+ })
77
+ .option('webhookUrl', {
78
+ alias: 't',
79
+ type: 'string',
80
+ description: 'Webhook URL',
81
+ })
82
+ .parse();
83
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import yargs from 'yargs/yargs';
3
+ import { hideBin } from 'yargs/helpers';
4
+ import { listUsers, listClients } from './src/cli.js';
5
+ import { convertJSON2CSV } from './lib/convert.js';
6
+ import { post2Webhook } from './lib/output.js';
7
+ class WebhookConfig {
8
+ constructor(type, url, title) {
9
+ this.type = type;
10
+ this.url = url;
11
+ this.title = title;
12
+ }
13
+ }
14
+ async function convert(format, output, config, json) {
15
+ let outputContent;
16
+ switch (format) {
17
+ case 'csv':
18
+ outputContent = (await convertJSON2CSV(json)).toString();
19
+ break;
20
+ // defaulting to JSON
21
+ default:
22
+ outputContent = JSON.stringify(json);
23
+ }
24
+ switch (output) {
25
+ case 'webhook':
26
+ try {
27
+ await post2Webhook(config.type, config.url, config.title, outputContent);
28
+ }
29
+ catch (e) {
30
+ console.error('Error during sending webhook: ', e);
31
+ }
32
+ break;
33
+ // defaulting to standard out
34
+ default:
35
+ console.log(outputContent);
36
+ }
37
+ }
38
+ yargs(hideBin(process.argv))
39
+ .command('listUsers [url] [clientId] [clientSecret]', 'fetches all users in the realms.',
40
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
41
+ () => { }, async (argv) => {
42
+ const users = await listUsers({
43
+ clientId: argv.clientId,
44
+ clientSecret: argv.clientSecret,
45
+ rootUrl: argv.url,
46
+ });
47
+ await convert(argv.format, argv.output, new WebhookConfig(argv.webhookType, argv.webhookUrl, 'User Listing'), users);
48
+ })
49
+ .command('listClients [url] [clientId] [clientSecret]', 'fetches all clients in the realms.',
50
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
51
+ () => { }, async (argv) => {
52
+ const clients = await listClients({
53
+ clientId: argv.clientId,
54
+ clientSecret: argv.clientSecret,
55
+ rootUrl: argv.url,
56
+ });
57
+ await convert(argv.format, argv.output, new WebhookConfig(argv.webhookType, argv.webhookUrl, 'Client Listing'), clients);
58
+ })
59
+ .option('format', {
60
+ alias: 'f',
61
+ type: 'string',
62
+ default: 'json',
63
+ description: 'output format, e.g. JSON|CSV',
64
+ })
65
+ .option('output', {
66
+ alias: 'o',
67
+ type: 'string',
68
+ default: 'stdout',
69
+ description: 'output channel',
70
+ })
71
+ .option('webhookType', {
72
+ alias: 'w',
73
+ type: 'string',
74
+ default: 'slack',
75
+ description: 'Webhook Type',
76
+ })
77
+ .option('webhookUrl', {
78
+ alias: 't',
79
+ type: 'string',
80
+ description: 'Webhook URL',
81
+ })
82
+ .parse();
83
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { Issuer } from 'openid-client';
2
+ import KcAdminClient from '@keycloak/keycloak-admin-client';
3
+ // Token refresh interval 60 seconds
4
+ const TOKEN_REFRESH = 60;
5
+ export async function createClient(options) {
6
+ const kcAdminClient = new KcAdminClient({
7
+ baseUrl: options.rootUrl,
8
+ realmName: 'master',
9
+ });
10
+ try {
11
+ // client login
12
+ await kcAdminClient.auth({
13
+ clientId: options.clientId,
14
+ clientSecret: options.clientSecret,
15
+ grantType: 'client_credentials',
16
+ });
17
+ }
18
+ catch (e) {
19
+ console.error('Check Client Config:', e.response.data.error_description);
20
+ return Promise.reject();
21
+ }
22
+ const keycloakIssuer = await Issuer.discover(`${options.rootUrl}/realms/master`);
23
+ const client = new keycloakIssuer.Client({
24
+ client_id: options.clientId,
25
+ token_endpoint_auth_method: 'none', // to send only client_id in the header
26
+ });
27
+ // Use the grant type 'password'
28
+ const tokenSet = await client.grant({
29
+ client_id: options.clientId,
30
+ client_secret: options.clientSecret,
31
+ grant_type: 'client_credentials',
32
+ });
33
+ /*
34
+ // TODO: FIXME - Periodically using refresh_token grant flow to get new access token here
35
+ setInterval(async () => {
36
+ const refreshToken = tokenSet.refresh_token;
37
+ kcAdminClient.setAccessToken((await client.refresh(refreshToken)).access_token);
38
+ }, TOKEN_REFRESH * 1000); */
39
+ return new Promise((resolve) => resolve(kcAdminClient));
40
+ }
41
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../lib/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,aAAa,MAAM,iCAAiC,CAAC;AAE5D,oCAAoC;AACpC,MAAM,aAAa,GAAG,EAAE,CAAC;AAQzB,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAgB;IACjD,MAAM,aAAa,GAAG,IAAI,aAAa,CAAC;QACtC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,SAAS,EAAE,QAAQ;KACpB,CAAC,CAAC;IAEH,IAAI;QACF,eAAe;QACf,MAAM,aAAa,CAAC,IAAI,CAAC;YACvB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,SAAS,EAAE,oBAAoB;SAChC,CAAC,CAAC;KACJ;IAAC,OAAO,CAAC,EAAE;QACV,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACxE,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC;KACzB;IAED,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,QAAQ,CAC1C,GAAG,OAAO,CAAC,OAAO,gBAAgB,CACnC,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC;QACvC,SAAS,EAAE,OAAO,CAAC,QAAQ;QAC3B,0BAA0B,EAAE,MAAM,EAAE,uCAAuC;KAC5E,CAAC,CAAC;IAEH,gCAAgC;IAChC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,SAAS,EAAE,OAAO,CAAC,QAAQ;QAC3B,aAAa,EAAE,OAAO,CAAC,YAAY;QACnC,UAAU,EAAE,oBAAoB;KACjC,CAAC,CAAC;IAEH;;;;;gCAK4B;IAE5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;AAC1D,CAAC"}