keycloakify 11.5.0 → 11.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keycloakify",
3
- "version": "11.5.0",
3
+ "version": "11.5.2",
4
4
  "description": "Framework to create custom Keycloak UIs",
5
5
  "repository": {
6
6
  "type": "git",
package/src/bin/main.ts CHANGED
@@ -5,9 +5,6 @@ import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
5
5
  import * as child_process from "child_process";
6
6
  import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
7
7
  import { getBuildContext } from "./shared/buildContext";
8
- import { SemVer } from "./tools/SemVer";
9
- import { assert, is } from "tsafe/assert";
10
- import chalk from "chalk";
11
8
 
12
9
  type CliCommandOptions = {
13
10
  projectDirPath: string | undefined;
@@ -137,47 +134,11 @@ program
137
134
  handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
138
135
  const { command } = await import("./start-keycloak");
139
136
 
140
- validate_keycloak_version: {
141
- if (keycloakVersion === undefined) {
142
- break validate_keycloak_version;
143
- }
144
-
145
- const isValidVersion = (() => {
146
- if (typeof keycloakVersion === "number") {
147
- return false;
148
- }
149
-
150
- try {
151
- SemVer.parse(keycloakVersion);
152
- } catch {
153
- return false;
154
- }
155
-
156
- return;
157
- })();
158
-
159
- if (isValidVersion) {
160
- break validate_keycloak_version;
161
- }
162
-
163
- console.log(
164
- chalk.red(
165
- [
166
- `Invalid Keycloak version: ${keycloakVersion}`,
167
- "It should be a valid semver version example: 26.0.4"
168
- ].join(" ")
169
- )
170
- );
171
-
172
- process.exit(1);
173
- }
174
-
175
- assert(is<string | undefined>(keycloakVersion));
176
-
177
137
  await command({
178
138
  buildContext: getBuildContext({ projectDirPath }),
179
139
  cliCommandOptions: {
180
- keycloakVersion,
140
+ keycloakVersion:
141
+ keycloakVersion === undefined ? undefined : `${keycloakVersion}`,
181
142
  port,
182
143
  realmJsonFilePath
183
144
  }
@@ -10,6 +10,7 @@ import { join as pathJoin, dirname as pathDirname } from "path";
10
10
  import * as fs from "fs/promises";
11
11
  import { existsAsync } from "../tools/fs.existsAsync";
12
12
  import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
13
+ import type { ReturnType } from "tsafe";
13
14
 
14
15
  export type BuildContextLike = {
15
16
  fetchOptions: BuildContext["fetchOptions"];
@@ -20,7 +21,10 @@ assert<BuildContext extends BuildContextLike ? true : false>;
20
21
 
21
22
  export async function getSupportedDockerImageTags(params: {
22
23
  buildContext: BuildContextLike;
23
- }) {
24
+ }): Promise<{
25
+ allSupportedTags: string[];
26
+ latestMajorTags: string[];
27
+ }> {
24
28
  const { buildContext } = params;
25
29
 
26
30
  {
@@ -31,14 +35,14 @@ export async function getSupportedDockerImageTags(params: {
31
35
  }
32
36
  }
33
37
 
34
- const tags: string[] = [];
38
+ const tags_queryResponse: string[] = [];
35
39
 
36
40
  await (async function callee(url: string) {
37
41
  const r = await fetch(url, buildContext.fetchOptions);
38
42
 
39
43
  await Promise.all([
40
44
  (async () => {
41
- tags.push(
45
+ tags_queryResponse.push(
42
46
  ...z
43
47
  .object({
44
48
  tags: z.array(z.string())
@@ -70,7 +74,9 @@ export async function getSupportedDockerImageTags(params: {
70
74
  ]);
71
75
  })("https://quay.io/v2/keycloak/keycloak/tags/list");
72
76
 
73
- const arr = tags
77
+ const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
78
+
79
+ const allSupportedTags_withVersion = tags_queryResponse
74
80
  .map(tag => ({
75
81
  tag,
76
82
  version: (() => {
@@ -86,28 +92,35 @@ export async function getSupportedDockerImageTags(params: {
86
92
  return undefined;
87
93
  }
88
94
 
95
+ if (tag.split(".").length !== 3) {
96
+ return undefined;
97
+ }
98
+
99
+ if (!supportedKeycloakMajorVersions.includes(version.major)) {
100
+ return undefined;
101
+ }
102
+
89
103
  return version;
90
104
  })()
91
105
  }))
92
106
  .map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
93
- .filter(exclude(undefined));
107
+ .filter(exclude(undefined))
108
+ .sort(({ version: a }, { version: b }) => SemVer.compare(b, a));
94
109
 
95
- const versionByMajor: Record<number, SemVer | undefined> = {};
110
+ const latestTagByMajor: Record<number, SemVer | undefined> = {};
96
111
 
97
- for (const { version } of arr) {
98
- const version_current = versionByMajor[version.major];
112
+ for (const { version } of allSupportedTags_withVersion) {
113
+ const version_current = latestTagByMajor[version.major];
99
114
 
100
115
  if (
101
116
  version_current === undefined ||
102
117
  SemVer.compare(version_current, version) === -1
103
118
  ) {
104
- versionByMajor[version.major] = version;
119
+ latestTagByMajor[version.major] = version;
105
120
  }
106
121
  }
107
122
 
108
- const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
109
-
110
- const result = Object.entries(versionByMajor)
123
+ const latestMajorTags = Object.entries(latestTagByMajor)
111
124
  .sort(([a], [b]) => parseInt(b) - parseInt(a))
112
125
  .map(([, version]) => version)
113
126
  .map(version => {
@@ -121,16 +134,40 @@ export async function getSupportedDockerImageTags(params: {
121
134
  })
122
135
  .filter(exclude(undefined));
123
136
 
137
+ const allSupportedTags = allSupportedTags_withVersion.map(({ tag }) => tag);
138
+
139
+ const result = {
140
+ latestMajorTags,
141
+ allSupportedTags
142
+ };
143
+
124
144
  await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
125
145
 
126
146
  return result;
127
147
  }
128
148
 
129
149
  const { getCachedValue, setCachedValue } = (() => {
150
+ type Result = ReturnType<typeof getSupportedDockerImageTags>;
151
+
152
+ const zResult = (() => {
153
+ type TargetType = Result;
154
+
155
+ const zTargetType = z.object({
156
+ allSupportedTags: z.array(z.string()),
157
+ latestMajorTags: z.array(z.string())
158
+ });
159
+
160
+ type InferredType = z.infer<typeof zTargetType>;
161
+
162
+ assert<Equals<TargetType, InferredType>>;
163
+
164
+ return id<z.ZodType<TargetType>>(zTargetType);
165
+ })();
166
+
130
167
  type Cache = {
131
168
  keycloakifyVersion: string;
132
169
  time: number;
133
- result: string[];
170
+ result: Result;
134
171
  };
135
172
 
136
173
  const zCache = (() => {
@@ -139,7 +176,7 @@ const { getCachedValue, setCachedValue } = (() => {
139
176
  const zTargetType = z.object({
140
177
  keycloakifyVersion: z.string(),
141
178
  time: z.number(),
142
- result: z.array(z.string())
179
+ result: zResult
143
180
  });
144
181
 
145
182
  type InferredType = z.infer<typeof zTargetType>;
@@ -39,7 +39,14 @@ export type ParsedRealmJson = {
39
39
  "post.logout.redirect.uris"?: string;
40
40
  };
41
41
  protocol?: string;
42
- protocolMappers?: unknown[];
42
+ protocolMappers?: {
43
+ id: string;
44
+ name: string;
45
+ protocol: string; // "openid-connect" or something else
46
+ protocolMapper: string; // "oidc-hardcoded-claim-mapper" or something else
47
+ consentRequired: boolean;
48
+ config?: Record<string, string>;
49
+ }[];
43
50
  }[];
44
51
  };
45
52
 
@@ -89,7 +96,18 @@ const zParsedRealmJson = (() => {
89
96
  })
90
97
  .optional(),
91
98
  protocol: z.string().optional(),
92
- protocolMappers: z.array(z.unknown()).optional()
99
+ protocolMappers: z
100
+ .array(
101
+ z.object({
102
+ id: z.string(),
103
+ name: z.string(),
104
+ protocol: z.string(),
105
+ protocolMapper: z.string(),
106
+ consentRequired: z.boolean(),
107
+ config: z.record(z.string()).optional()
108
+ })
109
+ )
110
+ .optional()
93
111
  })
94
112
  )
95
113
  });
@@ -985,6 +985,24 @@
985
985
  "claim.name": "locale",
986
986
  "jsonType.label": "String"
987
987
  }
988
+ },
989
+ {
990
+ "id": "8fd0d584-7052-4d04-a615-d18a71050873",
991
+ "name": "allowed-origins",
992
+ "protocol": "openid-connect",
993
+ "protocolMapper": "oidc-hardcoded-claim-mapper",
994
+ "consentRequired": false,
995
+ "config": {
996
+ "introspection.token.claim": "true",
997
+ "claim.value": "[\"*\"]",
998
+ "userinfo.token.claim": "true",
999
+ "id.token.claim": "false",
1000
+ "lightweight.claim": "false",
1001
+ "access.token.claim": "true",
1002
+ "claim.name": "allowed-origins",
1003
+ "jsonType.label": "JSON",
1004
+ "access.tokenResponse.claim": "false"
1005
+ }
988
1006
  }
989
1007
  ],
990
1008
  "defaultClientScopes": [
@@ -1640,13 +1658,13 @@
1640
1658
  "config": {
1641
1659
  "allowed-protocol-mapper-types": [
1642
1660
  "oidc-usermodel-property-mapper",
1643
- "saml-user-attribute-mapper",
1644
1661
  "saml-user-property-mapper",
1645
- "oidc-full-name-mapper",
1646
- "oidc-sha256-pairwise-sub-mapper",
1647
1662
  "oidc-address-mapper",
1663
+ "saml-user-attribute-mapper",
1664
+ "saml-role-list-mapper",
1665
+ "oidc-sha256-pairwise-sub-mapper",
1648
1666
  "oidc-usermodel-attribute-mapper",
1649
- "saml-role-list-mapper"
1667
+ "oidc-full-name-mapper"
1650
1668
  ]
1651
1669
  }
1652
1670
  },
@@ -1676,14 +1694,14 @@
1676
1694
  "subComponents": {},
1677
1695
  "config": {
1678
1696
  "allowed-protocol-mapper-types": [
1679
- "oidc-sha256-pairwise-sub-mapper",
1680
1697
  "saml-user-attribute-mapper",
1681
- "oidc-usermodel-property-mapper",
1682
1698
  "oidc-full-name-mapper",
1683
- "saml-role-list-mapper",
1699
+ "oidc-sha256-pairwise-sub-mapper",
1684
1700
  "saml-user-property-mapper",
1685
- "oidc-usermodel-attribute-mapper",
1686
- "oidc-address-mapper"
1701
+ "oidc-usermodel-property-mapper",
1702
+ "saml-role-list-mapper",
1703
+ "oidc-address-mapper",
1704
+ "oidc-usermodel-attribute-mapper"
1687
1705
  ]
1688
1706
  }
1689
1707
  },
@@ -1,6 +1,6 @@
1
1
  import { CONTAINER_NAME } from "../../shared/constants";
2
2
  import child_process from "child_process";
3
- import { join as pathJoin } from "path";
3
+ import { join as pathJoin, dirname as pathDirname, basename as pathBasename } from "path";
4
4
  import chalk from "chalk";
5
5
  import { Deferred } from "evt/tools/Deferred";
6
6
  import { assert, is } from "tsafe/assert";
@@ -20,16 +20,37 @@ export async function dumpContainerConfig(params: {
20
20
  }): Promise<ParsedRealmJson> {
21
21
  const { realmName, keycloakMajorVersionNumber, buildContext } = params;
22
22
 
23
- {
24
- // https://github.com/keycloak/keycloak/issues/33800
25
- const doesUseLockedH2Database = keycloakMajorVersionNumber >= 25;
23
+ // https://github.com/keycloak/keycloak/issues/33800
24
+ const doesUseLockedH2Database = keycloakMajorVersionNumber >= 25;
26
25
 
27
- if (doesUseLockedH2Database) {
28
- child_process.execSync(
29
- `docker exec ${CONTAINER_NAME} sh -c "cp -rp /opt/keycloak/data/h2 /tmp"`
30
- );
26
+ if (doesUseLockedH2Database) {
27
+ const dCompleted = new Deferred<void>();
28
+
29
+ const cmd = `docker exec ${CONTAINER_NAME} sh -c "cp -rp /opt/keycloak/data/h2 /tmp"`;
30
+
31
+ child_process.exec(cmd, error => {
32
+ if (error !== null) {
33
+ dCompleted.reject(error);
34
+ return;
35
+ }
36
+
37
+ dCompleted.resolve();
38
+ });
39
+
40
+ try {
41
+ await dCompleted.pr;
42
+ } catch (error) {
43
+ assert(is<Error>(error));
44
+
45
+ console.log(chalk.red(`Docker command failed: ${cmd}`));
46
+
47
+ console.log(chalk.red(error.message));
48
+
49
+ throw error;
31
50
  }
51
+ }
32
52
 
53
+ {
33
54
  const dCompleted = new Deferred<void>();
34
55
 
35
56
  const child = child_process.spawn(
@@ -56,7 +77,9 @@ export async function dumpContainerConfig(params: {
56
77
  let output = "";
57
78
 
58
79
  const onExit = (code: number | null) => {
59
- dCompleted.reject(new Error(`Exited with code ${code}`));
80
+ dCompleted.reject(
81
+ new Error(`docker exec kc.sh export command failed with code ${code}`)
82
+ );
60
83
  };
61
84
 
62
85
  child.once("exit", onExit);
@@ -96,25 +119,34 @@ export async function dumpContainerConfig(params: {
96
119
 
97
120
  console.log(output);
98
121
 
99
- process.exit(1);
122
+ throw error;
100
123
  }
124
+ }
101
125
 
102
- if (doesUseLockedH2Database) {
103
- const dCompleted = new Deferred<void>();
126
+ if (doesUseLockedH2Database) {
127
+ const dCompleted = new Deferred<void>();
104
128
 
105
- child_process.exec(
106
- `docker exec ${CONTAINER_NAME} sh -c "rm -rf /tmp/h2"`,
107
- error => {
108
- if (error !== null) {
109
- dCompleted.reject(error);
110
- return;
111
- }
129
+ const cmd = `docker exec ${CONTAINER_NAME} sh -c "rm -rf /tmp/h2"`;
112
130
 
113
- dCompleted.resolve();
114
- }
115
- );
131
+ child_process.exec(cmd, error => {
132
+ if (error !== null) {
133
+ dCompleted.reject(error);
134
+ return;
135
+ }
116
136
 
137
+ dCompleted.resolve();
138
+ });
139
+
140
+ try {
117
141
  await dCompleted.pr;
142
+ } catch (error) {
143
+ assert(is<Error>(error));
144
+
145
+ console.log(chalk.red(`Docker command failed: ${cmd}`));
146
+
147
+ console.log(chalk.red(error.message));
148
+
149
+ throw error;
118
150
  }
119
151
  }
120
152
 
@@ -126,8 +158,13 @@ export async function dumpContainerConfig(params: {
126
158
  {
127
159
  const dCompleted = new Deferred<void>();
128
160
 
161
+ const cmd = `docker cp ${CONTAINER_NAME}:/tmp/${realmName}-realm.json ${pathBasename(targetRealmConfigJsonFilePath_tmp)}`;
162
+
129
163
  child_process.exec(
130
- `docker cp ${CONTAINER_NAME}:/tmp/${realmName}-realm.json ${targetRealmConfigJsonFilePath_tmp}`,
164
+ cmd,
165
+ {
166
+ cwd: pathDirname(targetRealmConfigJsonFilePath_tmp)
167
+ },
131
168
  error => {
132
169
  if (error !== null) {
133
170
  dCompleted.reject(error);
@@ -138,7 +175,17 @@ export async function dumpContainerConfig(params: {
138
175
  }
139
176
  );
140
177
 
141
- await dCompleted.pr;
178
+ try {
179
+ await dCompleted.pr;
180
+ } catch (error) {
181
+ assert(is<Error>(error));
182
+
183
+ console.log(chalk.red(`Docker command failed: ${cmd}`));
184
+
185
+ console.log(chalk.red(error.message));
186
+
187
+ throw error;
188
+ }
142
189
  }
143
190
 
144
191
  return readRealmJsonFile({
@@ -276,7 +276,7 @@ function editAccountConsoleAndSecurityAdminConsole(params: {
276
276
  }) {
277
277
  const { parsedRealmJson } = params;
278
278
 
279
- for (const clientId of ["account-console", "security-admin-console"]) {
279
+ for (const clientId of ["account-console", "security-admin-console"] as const) {
280
280
  const client = parsedRealmJson.clients.find(
281
281
  client => client.clientId === clientId
282
282
  );
@@ -298,5 +298,68 @@ function editAccountConsoleAndSecurityAdminConsole(params: {
298
298
  (client.attributes ??= {})["post.logout.redirect.uris"] = "+";
299
299
 
300
300
  client.webOrigins = ["*"];
301
+
302
+ admin_specific: {
303
+ if (clientId !== "security-admin-console") {
304
+ break admin_specific;
305
+ }
306
+
307
+ const protocolMapper_preexisting = client.protocolMappers?.find(
308
+ protocolMapper => {
309
+ if (protocolMapper.protocolMapper !== "oidc-hardcoded-claim-mapper") {
310
+ return false;
311
+ }
312
+
313
+ if (protocolMapper.protocol !== "openid-connect") {
314
+ return false;
315
+ }
316
+
317
+ if (protocolMapper.config === undefined) {
318
+ return false;
319
+ }
320
+
321
+ if (protocolMapper.config["claim.name"] !== "allowed-origins") {
322
+ return false;
323
+ }
324
+
325
+ return true;
326
+ }
327
+ );
328
+
329
+ let protocolMapper: NonNullable<typeof protocolMapper_preexisting>;
330
+
331
+ const config = {
332
+ "introspection.token.claim": "true",
333
+ "claim.value": '["*"]',
334
+ "userinfo.token.claim": "true",
335
+ "id.token.claim": "false",
336
+ "lightweight.claim": "false",
337
+ "access.token.claim": "true",
338
+ "claim.name": "allowed-origins",
339
+ "jsonType.label": "JSON",
340
+ "access.tokenResponse.claim": "false"
341
+ };
342
+
343
+ if (protocolMapper_preexisting !== undefined) {
344
+ protocolMapper = protocolMapper_preexisting;
345
+ } else {
346
+ protocolMapper = {
347
+ id: "8fd0d584-7052-4d04-a615-d18a71050873",
348
+ name: "allowed-origins",
349
+ protocol: "openid-connect",
350
+ protocolMapper: "oidc-hardcoded-claim-mapper",
351
+ consentRequired: false,
352
+ config
353
+ };
354
+
355
+ (client.protocolMappers ??= []).push(protocolMapper);
356
+ }
357
+
358
+ assert(protocolMapper.config !== undefined);
359
+
360
+ if (config !== protocolMapper.config) {
361
+ Object.assign(protocolMapper.config, config);
362
+ }
363
+ }
301
364
  }
302
365
  }
@@ -105,11 +105,19 @@ export async function getRealmConfig(params: {
105
105
  chalk.grey(`Changes detected to the '${realmName}' config, backing up...`)
106
106
  );
107
107
 
108
- const parsedRealmJson = await dumpContainerConfig({
109
- buildContext,
110
- realmName,
111
- keycloakMajorVersionNumber
112
- });
108
+ let parsedRealmJson: ParsedRealmJson;
109
+
110
+ try {
111
+ parsedRealmJson = await dumpContainerConfig({
112
+ buildContext,
113
+ realmName,
114
+ keycloakMajorVersionNumber
115
+ });
116
+ } catch (error) {
117
+ console.log(chalk.red(`Failed to backup '${realmName}' config:`));
118
+
119
+ return;
120
+ }
113
121
 
114
122
  await writeRealmJsonFile({ parsedRealmJson });
115
123
 
@@ -97,7 +97,7 @@ export async function command(params: {
97
97
 
98
98
  const { cliCommandOptions, buildContext } = params;
99
99
 
100
- const availableTags = await getSupportedDockerImageTags({
100
+ const { allSupportedTags, latestMajorTags } = await getSupportedDockerImageTags({
101
101
  buildContext
102
102
  });
103
103
 
@@ -105,7 +105,7 @@ export async function command(params: {
105
105
  if (cliCommandOptions.keycloakVersion !== undefined) {
106
106
  const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
107
107
 
108
- const tag = availableTags.find(tag =>
108
+ const tag = allSupportedTags.find(tag =>
109
109
  tag.startsWith(cliCommandOptions_keycloakVersion)
110
110
  );
111
111
 
@@ -143,7 +143,7 @@ export async function command(params: {
143
143
  );
144
144
 
145
145
  const { value: tag } = await cliSelect<string>({
146
- values: availableTags
146
+ values: latestMajorTags
147
147
  }).catch(() => {
148
148
  process.exit(-1);
149
149
  });