Change structure
This commit is contained in:
123
src/bin/start-keycloak/realmConfig/ParsedRealmJson.ts
Normal file
123
src/bin/start-keycloak/realmConfig/ParsedRealmJson.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { z } from "zod";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { id } from "tsafe/id";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export type ParsedRealmJson = {
|
||||
name: string;
|
||||
users: {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
attributes: Record<string, unknown>;
|
||||
credentials: {
|
||||
type: string /* "password" or something else */;
|
||||
}[];
|
||||
clientRoles: Record<string, string[]>;
|
||||
}[];
|
||||
roles: {
|
||||
client: {
|
||||
name: string;
|
||||
containerId: string; // client id
|
||||
}[];
|
||||
};
|
||||
clients: {
|
||||
id: string;
|
||||
clientId: string; // example: realm-management
|
||||
baseUrl?: string;
|
||||
redirectUris?: string[];
|
||||
webOrigins?: string[];
|
||||
attributes?: {
|
||||
"post.logout.redirect.uris"?: string;
|
||||
};
|
||||
protocol?: string;
|
||||
protocolMappers?: unknown[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export function readRealmJsonFile(params: {
|
||||
realmJsonFilePath: string;
|
||||
}): ParsedRealmJson {
|
||||
const { realmJsonFilePath } = params;
|
||||
|
||||
const parsedRealmJson = JSON.parse(
|
||||
fs.readFileSync(realmJsonFilePath).toString("utf8")
|
||||
) as unknown;
|
||||
|
||||
const zParsedRealmJson = (() => {
|
||||
type TargetType = ParsedRealmJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
name: z.string(),
|
||||
users: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
username: z.string(),
|
||||
attributes: z.record(z.unknown()),
|
||||
credentials: z.array(
|
||||
z.object({
|
||||
type: z.string()
|
||||
})
|
||||
),
|
||||
clientRoles: z.record(z.array(z.string()))
|
||||
})
|
||||
),
|
||||
roles: z.object({
|
||||
client: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
containerId: z.string()
|
||||
})
|
||||
)
|
||||
}),
|
||||
clients: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
clientId: z.string(),
|
||||
baseUrl: z.string().optional(),
|
||||
redirectUris: z.array(z.string()).optional(),
|
||||
webOrigins: z.array(z.string()).optional(),
|
||||
attributes: z
|
||||
.object({
|
||||
"post.logout.redirect.uris": z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
protocol: z.string().optional(),
|
||||
protocolMappers: z.array(z.unknown()).optional()
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<TargetType, InferredType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
zParsedRealmJson.parse(parsedRealmJson);
|
||||
|
||||
assert(is<ParsedRealmJson>(parsedRealmJson));
|
||||
|
||||
return parsedRealmJson;
|
||||
}
|
||||
|
||||
export function getDefaultConfig(params: {
|
||||
keycloakMajorVersionNumber: number;
|
||||
}): ParsedRealmJson {
|
||||
const { keycloakMajorVersionNumber } = params;
|
||||
|
||||
const realmJsonFilePath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
||||
);
|
||||
|
||||
return readRealmJsonFile({ realmJsonFilePath });
|
||||
}
|
167
src/bin/start-keycloak/realmConfig/dumpContainerConfig.ts
Normal file
167
src/bin/start-keycloak/realmConfig/dumpContainerConfig.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
|
||||
import { CONTAINER_NAME } from "../../shared/constants";
|
||||
import child_process from "child_process";
|
||||
import { join as pathJoin } from "path";
|
||||
import chalk from "chalk";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert, is } from "tsafe/assert";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import * as fs from "fs/promises";
|
||||
|
||||
export type BuildContextLike = {
|
||||
cacheDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function dumpContainerConfig(params: {
|
||||
realmName: string;
|
||||
keycloakMajorVersionNumber: number;
|
||||
targetRealmConfigJsonFilePath: string;
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const {
|
||||
realmName,
|
||||
keycloakMajorVersionNumber,
|
||||
targetRealmConfigJsonFilePath,
|
||||
buildContext
|
||||
} = params;
|
||||
|
||||
{
|
||||
// https://github.com/keycloak/keycloak/issues/33800
|
||||
const doesUseLockedH2Database = keycloakMajorVersionNumber >= 26;
|
||||
|
||||
if (doesUseLockedH2Database) {
|
||||
child_process.execSync(
|
||||
`docker exec ${CONTAINER_NAME} sh -c "cp -rp /opt/keycloak/data/h2 /tmp"`
|
||||
);
|
||||
}
|
||||
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
const child = child_process.spawn(
|
||||
"docker",
|
||||
[
|
||||
...["exec", CONTAINER_NAME],
|
||||
...["/opt/keycloak/bin/kc.sh", "export"],
|
||||
...["--dir", "/tmp"],
|
||||
...["--realm", realmName],
|
||||
...["--users", "realm_file"],
|
||||
...(!doesUseLockedH2Database
|
||||
? []
|
||||
: [
|
||||
...["--db", "dev-file"],
|
||||
...[
|
||||
"--db-url",
|
||||
"'jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE'"
|
||||
]
|
||||
])
|
||||
],
|
||||
{ shell: true }
|
||||
);
|
||||
|
||||
let output = "";
|
||||
|
||||
const onExit = (code: number | null) => {
|
||||
dCompleted.reject(new Error(`Exited with code ${code}`));
|
||||
};
|
||||
|
||||
child.once("exit", onExit);
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
const outputStr = data.toString("utf8");
|
||||
|
||||
if (outputStr.includes("Export finished successfully")) {
|
||||
child.removeListener("exit", onExit);
|
||||
|
||||
// NOTE: On older Keycloak versions the process keeps running after the export is done.
|
||||
const timer = setTimeout(() => {
|
||||
child.removeListener("exit", onExit2);
|
||||
child.kill();
|
||||
dCompleted.resolve();
|
||||
}, 1500);
|
||||
|
||||
const onExit2 = () => {
|
||||
clearTimeout(timer);
|
||||
dCompleted.resolve();
|
||||
};
|
||||
|
||||
child.once("exit", onExit2);
|
||||
}
|
||||
|
||||
output += outputStr;
|
||||
});
|
||||
|
||||
child.stderr.on("data", data => (output += chalk.red(data.toString("utf8"))));
|
||||
|
||||
try {
|
||||
await dCompleted.pr;
|
||||
} catch (error) {
|
||||
assert(is<Error>(error));
|
||||
|
||||
console.log(chalk.red(error.message));
|
||||
|
||||
console.log(output);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (doesUseLockedH2Database) {
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
child_process.exec(
|
||||
`docker exec ${CONTAINER_NAME} sh -c "rm -rf /tmp/h2"`,
|
||||
error => {
|
||||
if (error !== null) {
|
||||
dCompleted.reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dCompleted.resolve();
|
||||
}
|
||||
);
|
||||
|
||||
await dCompleted.pr;
|
||||
}
|
||||
}
|
||||
|
||||
const targetRealmConfigJsonFilePath_tmp = pathJoin(
|
||||
buildContext.cacheDirPath,
|
||||
"realm.json"
|
||||
);
|
||||
|
||||
{
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
child_process.exec(
|
||||
`docker cp ${CONTAINER_NAME}:/tmp/${realmName}-realm.json ${targetRealmConfigJsonFilePath_tmp}`,
|
||||
error => {
|
||||
if (error !== null) {
|
||||
dCompleted.reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dCompleted.resolve();
|
||||
}
|
||||
);
|
||||
|
||||
await dCompleted.pr;
|
||||
}
|
||||
|
||||
let sourceCode = (await fs.readFile(targetRealmConfigJsonFilePath_tmp)).toString(
|
||||
"utf8"
|
||||
);
|
||||
|
||||
run_prettier: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break run_prettier;
|
||||
}
|
||||
|
||||
sourceCode = await runPrettier({
|
||||
filePath: targetRealmConfigJsonFilePath,
|
||||
sourceCode: sourceCode
|
||||
});
|
||||
}
|
||||
|
||||
await fs.writeFile(targetRealmConfigJsonFilePath, Buffer.from(sourceCode, "utf8"));
|
||||
}
|
271
src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts
Normal file
271
src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts
Normal file
@ -0,0 +1,271 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import { getDefaultConfig, type ParsedRealmJson } from "./ParsedRealmJson";
|
||||
|
||||
export function prepareRealmConfig(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
keycloakMajorVersionNumber: number;
|
||||
}): {
|
||||
realmName: string;
|
||||
clientName: string;
|
||||
username: string;
|
||||
} {
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||
|
||||
const { username } = addOrEditTestUser({
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
|
||||
const { clientId } = addOrEditClient({
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
|
||||
editAccountConsoleAndSecurityAdminConsole({ parsedRealmJson });
|
||||
|
||||
return {
|
||||
realmName: parsedRealmJson.name,
|
||||
clientName: clientId,
|
||||
username
|
||||
};
|
||||
}
|
||||
|
||||
function addOrEditTestUser(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
keycloakMajorVersionNumber: number;
|
||||
}): { username: string } {
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||
|
||||
const parsedRealmJson_default = getDefaultConfig({ keycloakMajorVersionNumber });
|
||||
|
||||
const [defaultUser_default] = parsedRealmJson_default.users;
|
||||
|
||||
assert(defaultUser_default !== undefined);
|
||||
|
||||
const defaultUser_preexisting = parsedRealmJson.users.find(
|
||||
user => user.username === defaultUser_default.username
|
||||
);
|
||||
|
||||
const newUser = structuredClone(
|
||||
defaultUser_preexisting ??
|
||||
(() => {
|
||||
const firstUser = parsedRealmJson.users[0];
|
||||
|
||||
if (firstUser === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const firstUserCopy = structuredClone(firstUser);
|
||||
|
||||
firstUserCopy.id = defaultUser_default.id;
|
||||
|
||||
return firstUserCopy;
|
||||
})() ??
|
||||
defaultUser_default
|
||||
);
|
||||
|
||||
newUser.username = defaultUser_default.username;
|
||||
newUser.email = defaultUser_default.email;
|
||||
|
||||
delete_existing_password_credential_if_any: {
|
||||
const i = newUser.credentials.findIndex(
|
||||
credential => credential.type === "password"
|
||||
);
|
||||
|
||||
if (i === -1) {
|
||||
break delete_existing_password_credential_if_any;
|
||||
}
|
||||
|
||||
newUser.credentials.splice(i, 1);
|
||||
}
|
||||
|
||||
{
|
||||
const credential = defaultUser_default.credentials.find(
|
||||
credential => credential.type === "password"
|
||||
);
|
||||
|
||||
assert(credential !== undefined);
|
||||
|
||||
newUser.credentials.push(credential);
|
||||
}
|
||||
|
||||
{
|
||||
const nameByClientId = Object.fromEntries(
|
||||
parsedRealmJson.clients.map(client => [client.id, client.clientId] as const)
|
||||
);
|
||||
|
||||
newUser.clientRoles = {};
|
||||
|
||||
for (const clientRole of parsedRealmJson.roles.client) {
|
||||
const clientName = nameByClientId[clientRole.containerId];
|
||||
|
||||
assert(clientName !== undefined);
|
||||
|
||||
(newUser.clientRoles[clientName] ??= []).push(clientRole.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultUser_preexisting === undefined) {
|
||||
parsedRealmJson.users.push(newUser);
|
||||
} else {
|
||||
const i = parsedRealmJson.users.indexOf(defaultUser_preexisting);
|
||||
assert(i !== -1);
|
||||
parsedRealmJson.users[i] = newUser;
|
||||
}
|
||||
|
||||
return { username: newUser.username };
|
||||
}
|
||||
|
||||
const TEST_APP_URL = "https://my-theme.keycloakify.dev";
|
||||
|
||||
function addOrEditClient(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
keycloakMajorVersionNumber: number;
|
||||
}): { clientId: string } {
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||
|
||||
const parsedRealmJson_default = getDefaultConfig({ keycloakMajorVersionNumber });
|
||||
|
||||
const testClient_default = (() => {
|
||||
const clients = parsedRealmJson_default.clients.filter(client => {
|
||||
return JSON.stringify(client).includes(TEST_APP_URL);
|
||||
});
|
||||
|
||||
assert(clients.length === 1);
|
||||
|
||||
return clients[0];
|
||||
})();
|
||||
|
||||
const clientIds_builtIn = parsedRealmJson_default.clients
|
||||
.map(client => client.clientId)
|
||||
.filter(clientId => clientId !== testClient_default.clientId);
|
||||
|
||||
const testClient_preexisting = (() => {
|
||||
const clients = parsedRealmJson.clients
|
||||
.filter(client => !clientIds_builtIn.includes(client.clientId))
|
||||
.filter(client => client.protocol === "openid-connect");
|
||||
|
||||
{
|
||||
const client = clients.find(
|
||||
client => client.clientId === testClient_default.clientId
|
||||
);
|
||||
|
||||
if (client !== undefined) {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const client = clients.find(
|
||||
client =>
|
||||
client.redirectUris?.find(redirectUri =>
|
||||
redirectUri.startsWith(TEST_APP_URL)
|
||||
) !== undefined
|
||||
);
|
||||
|
||||
if (client !== undefined) {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
const [client] = clients;
|
||||
|
||||
if (client === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return client;
|
||||
})();
|
||||
|
||||
let testClient: typeof testClient_default;
|
||||
|
||||
if (testClient_preexisting !== undefined) {
|
||||
testClient = testClient_preexisting;
|
||||
} else {
|
||||
testClient = structuredClone(testClient_default);
|
||||
delete testClient.protocolMappers;
|
||||
parsedRealmJson.clients.push(testClient);
|
||||
}
|
||||
|
||||
{
|
||||
for (const redirectUri of [
|
||||
`${TEST_APP_URL}/*`,
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*"
|
||||
]) {
|
||||
for (const propertyName of ["webOrigins", "redirectUris"] as const) {
|
||||
const arr = (testClient[propertyName] ??= []);
|
||||
|
||||
if (arr.includes(redirectUri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
arr.push(redirectUri);
|
||||
}
|
||||
|
||||
{
|
||||
if (testClient.attributes === undefined) {
|
||||
testClient.attributes = {};
|
||||
}
|
||||
|
||||
const arr = (testClient.attributes["post.logout.redirect.uris"] ?? "")
|
||||
.split("##")
|
||||
.map(s => s.trim());
|
||||
|
||||
if (!arr.includes(redirectUri)) {
|
||||
arr.push(redirectUri);
|
||||
testClient.attributes["post.logout.redirect.uris"] = arr.join("##");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { clientId: testClient.clientId };
|
||||
}
|
||||
|
||||
function editAccountConsoleAndSecurityAdminConsole(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
}) {
|
||||
const { parsedRealmJson } = params;
|
||||
|
||||
for (const clientId of ["account-console", "security-admin-console"]) {
|
||||
const client = parsedRealmJson.clients.find(
|
||||
client => client.clientId === clientId
|
||||
);
|
||||
|
||||
assert(client !== undefined);
|
||||
|
||||
{
|
||||
for (const redirectUri of [
|
||||
`${TEST_APP_URL}/*`,
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*"
|
||||
]) {
|
||||
for (const propertyName of ["webOrigins", "redirectUris"] as const) {
|
||||
const arr = (client[propertyName] ??= []);
|
||||
|
||||
if (arr.includes(redirectUri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
arr.push(redirectUri);
|
||||
}
|
||||
|
||||
{
|
||||
if (client.attributes === undefined) {
|
||||
client.attributes = {};
|
||||
}
|
||||
|
||||
const arr = (client.attributes["post.logout.redirect.uris"] ?? "")
|
||||
.split("##")
|
||||
.map(s => s.trim());
|
||||
|
||||
if (!arr.includes(redirectUri)) {
|
||||
arr.push(redirectUri);
|
||||
client.attributes["post.logout.redirect.uris"] = arr.join("##");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2155
src/bin/start-keycloak/realmConfig/realm-kc-18.json
Normal file
2155
src/bin/start-keycloak/realmConfig/realm-kc-18.json
Normal file
File diff suppressed because it is too large
Load Diff
2186
src/bin/start-keycloak/realmConfig/realm-kc-19.json
Normal file
2186
src/bin/start-keycloak/realmConfig/realm-kc-19.json
Normal file
File diff suppressed because it is too large
Load Diff
2197
src/bin/start-keycloak/realmConfig/realm-kc-20.json
Normal file
2197
src/bin/start-keycloak/realmConfig/realm-kc-20.json
Normal file
File diff suppressed because it is too large
Load Diff
2201
src/bin/start-keycloak/realmConfig/realm-kc-21.json
Normal file
2201
src/bin/start-keycloak/realmConfig/realm-kc-21.json
Normal file
File diff suppressed because it is too large
Load Diff
2155
src/bin/start-keycloak/realmConfig/realm-kc-23.json
Normal file
2155
src/bin/start-keycloak/realmConfig/realm-kc-23.json
Normal file
File diff suppressed because it is too large
Load Diff
2318
src/bin/start-keycloak/realmConfig/realm-kc-24.json
Normal file
2318
src/bin/start-keycloak/realmConfig/realm-kc-24.json
Normal file
File diff suppressed because it is too large
Load Diff
2387
src/bin/start-keycloak/realmConfig/realm-kc-25.json
Normal file
2387
src/bin/start-keycloak/realmConfig/realm-kc-25.json
Normal file
File diff suppressed because it is too large
Load Diff
2548
src/bin/start-keycloak/realmConfig/realm-kc-26.json
Normal file
2548
src/bin/start-keycloak/realmConfig/realm-kc-26.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user