diff --git a/.gitignore b/.gitignore index a732d8e940f..6756eda69dd 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,9 @@ node # Vite dist +!/quarkus/dist +!/quarkus/**/src/**/dist + # ESLint .eslintcache @@ -108,4 +111,4 @@ node_modules .sdkmanrc # JENV -.java-version \ No newline at end of file +.java-version diff --git a/common/src/main/java/org/keycloak/common/util/IoUtils.java b/common/src/main/java/org/keycloak/common/util/IoUtils.java new file mode 100644 index 00000000000..ed16843603e --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/IoUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.common.util; + +import java.io.Console; + +public class IoUtils { + + public static String readFromConsole(String kind, String defaultValue, boolean password) { + Console cons = System.console(); + if (cons == null) { + if (defaultValue != null) { + return defaultValue; + } + throw new RuntimeException(String.format("Console is not active, but %s is required", kind)); + } + String prompt = String.format("Enter %s", kind) + (defaultValue != null ? String.format(" [%s]:", defaultValue) : ":"); + if (password) { + char[] passwd; + if ((passwd = cons.readPassword(prompt)) != null) { + return new String(passwd); + } + } else { + return cons.readLine(prompt); + } + throw new RuntimeException(String.format("No %s provided", kind)); + } + + public static String readPasswordFromConsole(String kind) { + return readFromConsole(kind, null, true); + } + + public static String readLineFromConsole(String kind, String defaultValue) { + return readFromConsole(kind, defaultValue, false); + } + +} diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java index 2446070b173..9a72ddcf01e 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java @@ -28,9 +28,9 @@ import static org.keycloak.client.admin.cli.operations.UserOperations.getIdFromU import static org.keycloak.client.admin.cli.operations.UserOperations.resetUserPassword; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig; -import static org.keycloak.client.cli.util.IoUtil.readSecret; import static org.keycloak.client.cli.util.OsUtil.PROMPT; import static org.keycloak.client.admin.cli.KcAdmMain.CMD; +import static org.keycloak.common.util.IoUtils.readPasswordFromConsole; /** * @author Marko Strukelj @@ -61,7 +61,7 @@ public class SetPasswordCmd extends AbstractAuthOptionsCmd { } if (pass == null) { - pass = readSecret("Enter password: "); + pass = readPasswordFromConsole("password"); } ConfigData config = loadConfig(); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseAuthOptionsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseAuthOptionsCmd.java index 3b7a8bfdde7..5e66e312e99 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseAuthOptionsCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseAuthOptionsCmd.java @@ -25,7 +25,7 @@ import org.keycloak.client.cli.config.RealmConfigData; import org.keycloak.client.cli.util.AuthUtil; import org.keycloak.client.cli.util.ConfigUtil; import org.keycloak.client.cli.util.HttpUtil; -import org.keycloak.client.cli.util.IoUtil; +import org.keycloak.common.util.IoUtils; import java.io.File; import java.io.PrintWriter; @@ -176,8 +176,8 @@ public abstract class BaseAuthOptionsCmd extends BaseGlobalOptionsCmd { if (pass == null) { pass = System.getenv("KC_CLI_TRUSTSTORE_PASSWORD"); } - if (pass == null) { - pass = IoUtil.readSecret("Enter truststore password: "); + if (pass == null) { + pass = IoUtils.readPasswordFromConsole("truststore password"); } try { diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseConfigCredentialsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseConfigCredentialsCmd.java index dcf6f839372..63d9b143679 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseConfigCredentialsCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseConfigCredentialsCmd.java @@ -34,10 +34,9 @@ import static org.keycloak.client.cli.util.ConfigUtil.getHandler; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.ConfigUtil.saveTokens; import static org.keycloak.client.cli.util.IoUtil.printErr; -import static org.keycloak.client.cli.util.IoUtil.readSecret; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.cli.util.OsUtil.PROMPT; - +import static org.keycloak.common.util.IoUtils.readPasswordFromConsole; /** * @author Marko Strukelj @@ -107,11 +106,11 @@ public class BaseConfigCredentialsCmd extends BaseAuthOptionsCmd { password = System.getenv("KC_CLI_PASSWORD"); } if (password == null) { - password = readSecret("Enter password: "); + password = readPasswordFromConsole("password"); } // if secret was set to be read from stdin, then ask for it if ("-".equals(secret) && keystore == null) { - secret = readSecret("Enter client secret: "); + secret = readPasswordFromConsole("client secret"); } } else if (keystore != null || secret != null || clientSet) { grantTypeForAuthentication = OAuth2Constants.CLIENT_CREDENTIALS; @@ -119,7 +118,7 @@ public class BaseConfigCredentialsCmd extends BaseAuthOptionsCmd { if (keystore == null && secret == null) { secret = System.getenv("KC_CLI_CLIENT_SECRET"); if (secret == null) { - secret = readSecret("Enter client secret: "); + secret = readPasswordFromConsole("client secret"); } } } @@ -141,9 +140,9 @@ public class BaseConfigCredentialsCmd extends BaseAuthOptionsCmd { } if (storePass == null) { - storePass = readSecret("Enter keystore password: "); + storePass = readPasswordFromConsole("keystore password"); if (keyPass == null) { - keyPass = readSecret("Enter key password: "); + keyPass = readPasswordFromConsole("key password"); } } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseConfigTruststoreCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseConfigTruststoreCmd.java index 8eaa2185790..207af4bb61e 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseConfigTruststoreCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/common/BaseConfigTruststoreCmd.java @@ -24,9 +24,9 @@ import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig; -import static org.keycloak.client.cli.util.IoUtil.readSecret; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.cli.util.OsUtil.PROMPT; +import static org.keycloak.common.util.IoUtils.readPasswordFromConsole; /** * @author Marko Strukelj @@ -78,7 +78,7 @@ public class BaseConfigTruststoreCmd extends BaseAuthOptionsCmd { } if ("-".equals(trustPass)) { - trustPass = readSecret("Enter truststore password: "); + trustPass = readPasswordFromConsole("truststore password"); } pass = trustPass; diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/util/IoUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/util/IoUtil.java index adde5df289d..cc1a39a9eea 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/util/IoUtil.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/util/IoUtil.java @@ -16,7 +16,8 @@ */ package org.keycloak.client.cli.util; -import java.io.Console; +import org.keycloak.common.util.StreamUtil; + import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -65,31 +66,12 @@ public class IoUtil { return content; } - public static String readSecret(String prompt) { - Console cons = System.console(); - if (cons == null) { - throw new RuntimeException("Console is not active, but a password is required"); - } - char[] passwd; - if ((passwd = cons.readPassword("%s", prompt)) != null) { - return new String(passwd); - } - throw new RuntimeException("No password provided"); - } - public static String readFully(InputStream is) { - StringBuilder out = new StringBuilder(); - byte [] buf = new byte[8192]; - - int rc; try { - while ((rc = is.read(buf)) != -1) { - out.append(new String(buf, 0, rc, StandardCharsets.UTF_8)); - } + return StreamUtil.readString(is, StandardCharsets.UTF_8); } catch (Exception e) { throw new RuntimeException("Failed to read stream", e); } - return out.toString(); } public static void copyStream(InputStream is, OutputStream os) { diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java index 31fdcb0b3f7..d2d22898c25 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java @@ -1,9 +1,9 @@ package org.keycloak.client.registration.cli.commands; import org.keycloak.client.cli.config.RealmConfigData; -import org.keycloak.client.cli.util.IoUtil; import org.keycloak.client.registration.cli.CmdStdinContext; import org.keycloak.client.registration.cli.KcRegMain; +import org.keycloak.common.util.IoUtils; import java.io.PrintWriter; import java.io.StringWriter; @@ -64,7 +64,7 @@ public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd { } if (!delete && token == null) { - token = IoUtil.readSecret("Enter Initial Access Token: "); + token = IoUtils.readPasswordFromConsole("Initial Access Token"); } // now update the config diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java index d78ddf56fc9..41a11e7253e 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java @@ -1,8 +1,8 @@ package org.keycloak.client.registration.cli.commands; import org.keycloak.client.registration.cli.KcRegMain; +import org.keycloak.common.util.IoUtils; import org.keycloak.client.cli.config.RealmConfigData; -import org.keycloak.client.cli.util.IoUtil; import java.io.PrintWriter; import java.io.StringWriter; @@ -61,7 +61,7 @@ public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd { if (!delete && token == null) { - token = IoUtil.readSecret("Enter Registration Access Token: "); + token = IoUtils.readPasswordFromConsole("Registration Access Token"); } // now update the config diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java index 48a2ca9938c..4212c0d4692 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java @@ -48,7 +48,6 @@ import static org.keycloak.client.cli.util.HttpUtil.doPost; import static org.keycloak.client.cli.util.IoUtil.printErr; import static org.keycloak.client.cli.util.IoUtil.printOut; import static org.keycloak.client.cli.util.IoUtil.readFully; -import static org.keycloak.client.cli.util.IoUtil.readSecret; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.cli.util.OsUtil.PROMPT; import static org.keycloak.client.cli.util.ParseUtil.parseKeyVal; @@ -56,6 +55,7 @@ import static org.keycloak.client.registration.cli.EndpointType.DEFAULT; import static org.keycloak.client.registration.cli.EndpointType.OIDC; import static org.keycloak.client.registration.cli.EndpointType.SAML2; import static org.keycloak.client.registration.cli.KcRegMain.CMD; +import static org.keycloak.common.util.IoUtils.readPasswordFromConsole; /** * @author Marko Strukelj @@ -105,7 +105,7 @@ public class CreateCmd extends AbstractAuthOptionsCmd { // if --token is specified read it if ("-".equals(externalToken)) { - externalToken = readSecret("Enter Initial Access Token: "); + externalToken = readPasswordFromConsole("Initial Access Token"); } CmdStdinContext ctx = new CmdStdinContext(); diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/BootstrapAdminOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/BootstrapAdminOptions.java new file mode 100644 index 00000000000..21a7305001a --- /dev/null +++ b/quarkus/config-api/src/main/java/org/keycloak/config/BootstrapAdminOptions.java @@ -0,0 +1,35 @@ +package org.keycloak.config; + +public class BootstrapAdminOptions { + + public static final Option PASSWORD = new OptionBuilder<>("bootstrap-admin-password", String.class) + .category(OptionCategory.BOOTSTRAP_ADMIN) + .description("Bootstrap admin password") + .hidden() + .build(); + + public static final Option USERNAME = new OptionBuilder<>("bootstrap-admin-username", String.class) + .category(OptionCategory.BOOTSTRAP_ADMIN) + .description("Username of the bootstrap admin") + .hidden() + .build(); + + public static final Option EXPIRATION = new OptionBuilder<>("bootstrap-admin-expiration", Integer.class) + .category(OptionCategory.BOOTSTRAP_ADMIN) + .description("Time in minutes for the bootstrap admin user to expire.") + .hidden() + .build(); + + public static final Option CLIENT_ID = new OptionBuilder<>("bootstrap-admin-client-id", String.class) + .category(OptionCategory.BOOTSTRAP_ADMIN) + .description("Client id for the admin service") + .hidden() + .build(); + + public static final Option CLIENT_SECRET = new OptionBuilder<>("bootstrap-admin-client-secret", String.class) + .category(OptionCategory.BOOTSTRAP_ADMIN) + .description("Client secret for the admin service") + .hidden() + .build(); + +} diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java b/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java index 339c9f9938c..5c90b1553fe 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java @@ -1,7 +1,6 @@ package org.keycloak.config; public enum OptionCategory { - // ordered by name asc CACHE("Cache", 10, ConfigSupportLevel.SUPPORTED), CONFIG("Config", 15, ConfigSupportLevel.SUPPORTED), DATABASE("Database", 20, ConfigSupportLevel.SUPPORTED), @@ -20,6 +19,7 @@ public enum OptionCategory { SECURITY("Security", 120, ConfigSupportLevel.SUPPORTED), EXPORT("Export", 130, ConfigSupportLevel.SUPPORTED), IMPORT("Import", 140, ConfigSupportLevel.SUPPORTED), + BOOTSTRAP_ADMIN("Bootstrap Admin", 998, ConfigSupportLevel.SUPPORTED), GENERAL("General", 999, ConfigSupportLevel.SUPPORTED); private final String heading; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java index b8a20581ab2..9f8337e9480 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java @@ -42,7 +42,9 @@ import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; public final class Environment { - public static final String IMPORT_EXPORT_MODE = "import_export"; + public static final String NON_SERVER_MODE = "nonserver"; + public static final String PROFILE ="kc.profile"; + public static final String ENV_PROFILE ="KC_PROFILE"; public static final String DATA_PATH = File.separator + "data"; public static final String DEFAULT_THEMES_PATH = File.separator + "themes"; public static final String PROD_PROFILE_VALUE = "prod"; @@ -139,8 +141,8 @@ public final class Environment { return Optional.ofNullable(org.keycloak.common.util.Environment.getProfile()).orElse("").equalsIgnoreCase(org.keycloak.common.util.Environment.DEV_PROFILE_VALUE); } - public static boolean isImportExportMode() { - return IMPORT_EXPORT_MODE.equalsIgnoreCase(org.keycloak.common.util.Environment.getProfile()); + public static boolean isNonServerMode() { + return NON_SERVER_MODE.equalsIgnoreCase(org.keycloak.common.util.Environment.getProfile()); } public static boolean isWindows() { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java index 9a4e05a64d9..1b3cb7dd429 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java @@ -20,7 +20,7 @@ package org.keycloak.quarkus.runtime; import static org.keycloak.quarkus.runtime.Environment.getKeycloakModeFromProfile; import static org.keycloak.quarkus.runtime.Environment.isDevProfile; import static org.keycloak.quarkus.runtime.Environment.getProfileOrDefault; -import static org.keycloak.quarkus.runtime.Environment.isImportExportMode; +import static org.keycloak.quarkus.runtime.Environment.isNonServerMode; import static org.keycloak.quarkus.runtime.Environment.isTestLaunchMode; import static org.keycloak.quarkus.runtime.cli.Picocli.parseAndRun; import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG; @@ -168,7 +168,7 @@ public class KeycloakMain implements QuarkusApplication { int exitCode = ApplicationLifecycleManager.getExitCode(); - if (isTestLaunchMode() || isImportExportMode()) { + if (isTestLaunchMode() || isNonServerMode()) { // in test mode we exit immediately // we should be managing this behavior more dynamically depending on the tests requirements (short/long lived) Quarkus.asyncExit(exitCode); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Help.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Help.java index 40b199a78e4..dda1e0d9caa 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Help.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Help.java @@ -43,7 +43,7 @@ public final class Help extends CommandLine.Help { private static final int HELP_WIDTH = 100; private static final String DEFAULT_OPTION_LIST_HEADING = "Options:"; private static final String DEFAULT_COMMAND_LIST_HEADING = "Commands:"; - private boolean allOptions; + private static boolean ALL_OPTIONS; Help(CommandLine.Model.CommandSpec commandSpec, ColorScheme colorScheme) { super(commandSpec, colorScheme); @@ -165,7 +165,7 @@ public final class Help extends CommandLine.Help { String optionName = undecorateDuplicitOptionName(option.longestName()); OptionCategory category = null; - if (option.group() != null) { + if (option.group() != null && option.group().heading() != null) { category = OptionCategory.fromHeading(removeSuffix(option.group().heading(), ":")); } PropertyMapper mapper = getMapper(optionName, category); @@ -180,7 +180,7 @@ public final class Help extends CommandLine.Help { return true; } - if (allOptions && isDisabledMapper) { + if (ALL_OPTIONS && isDisabledMapper) { return true; } @@ -192,13 +192,13 @@ public final class Help extends CommandLine.Help { if (isUnsupportedOption) { // unsupported options removed from help if all options are not requested - return !option.hidden() && allOptions; + return !option.hidden() && ALL_OPTIONS; } return !option.hidden(); } - public void setAllOptions(boolean allOptions) { - this.allOptions = allOptions; + public static void setAllOptions(boolean allOptions) { + ALL_OPTIONS = allOptions; } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/HelpFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/HelpFactory.java index ab42699f041..a3cdf8147b4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/HelpFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/HelpFactory.java @@ -23,14 +23,9 @@ import picocli.CommandLine.Model.CommandSpec; final class HelpFactory implements CommandLine.IHelpFactory { - private Help help; - @Override public CommandLine.Help create(CommandSpec commandSpec, ColorScheme colorScheme) { - if (help == null) { - help = new Help(commandSpec, colorScheme); - } - return help; + return new Help(commandSpec, colorScheme); } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java index b7b2807bb1d..b3e24747d8f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java @@ -53,6 +53,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -63,6 +64,7 @@ import org.keycloak.config.DeprecatedMetadata; import org.keycloak.config.Option; import org.keycloak.config.OptionCategory; import org.keycloak.quarkus.runtime.cli.command.AbstractCommand; +import org.keycloak.quarkus.runtime.cli.command.BootstrapAdmin; import org.keycloak.quarkus.runtime.cli.command.Build; import org.keycloak.quarkus.runtime.cli.command.ImportRealmMixin; import org.keycloak.quarkus.runtime.cli.command.Main; @@ -85,8 +87,11 @@ import io.smallrye.config.ConfigValue; import picocli.CommandLine; import picocli.CommandLine.ParameterException; +import picocli.CommandLine.ParseResult; +import picocli.CommandLine.DuplicateOptionAnnotationsException; import picocli.CommandLine.Help.Ansi; import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.ISetter; import picocli.CommandLine.Model.OptionSpec; import picocli.CommandLine.Model.ArgGroupSpec; @@ -106,15 +111,30 @@ public final class Picocli { } public static void parseAndRun(List cliArgs) { - CommandLine cmd = createCommandLine(cliArgs); + // perform two passes over the cli args. First without option validation to determine the current command, then with option validation enabled + CommandLine cmd = createCommandLine(spec -> spec + .addUnmatchedArgsBinding(CommandLine.Model.UnmatchedArgsBinding.forStringArrayConsumer(new ISetter() { + @Override + public T set(T value) throws Exception { + return null; // just ignore + } + }))); String[] argArray = cliArgs.toArray(new String[0]); try { - cmd.parseArgs(argArray); // process the cli args first to init the config file and perform validation + ParseResult result = cmd.parseArgs(argArray); // process the cli args first to init the config file and perform validation + var commandLineList = result.asCommandLineList(); + + // recreate the command specifically for the current + cmd = createCommandLineForCommand(cliArgs, commandLineList); int exitCode; if (isRebuildCheck()) { - exitCode = runReAugmentationIfNeeded(cliArgs, cmd); + CommandLine currentCommand = null; + if (commandLineList.size() > 1) { + currentCommand = commandLineList.get(commandLineList.size() - 1); + } + exitCode = runReAugmentationIfNeeded(cliArgs, cmd, currentCommand); } else { PropertyMappers.sanitizeDisabledMappers(); exitCode = cmd.execute(argArray); @@ -128,6 +148,37 @@ public final class Picocli { } } + private static CommandLine createCommandLineForCommand(List cliArgs, List commandLineList) { + return createCommandLine(spec -> { + // use the incoming commandLineList from the initial parsing to determine the current command + CommandSpec currentSpec = spec; + + // add help to the root and all commands as it is not inherited + addHelp(currentSpec); + + for (CommandLine commandLine : commandLineList.subList(1, commandLineList.size())) { + CommandLine subCommand = currentSpec.subcommands().get(commandLine.getCommandName()); + if (subCommand == null) { + currentSpec = null; + break; + } + + currentSpec = subCommand.getCommandSpec(); + + addHelp(currentSpec); + } + + if (currentSpec != null) { + addCommandOptions(cliArgs, currentSpec.commandLine()); + } + + if (isRebuildCheck()) { + // build command should be available when running re-aug + addCommandOptions(cliArgs, spec.subcommands().get(Build.NAME)); + } + }); + } + private static void catchParameterException(ParameterException parEx, CommandLine cmd, String[] args) { int exitCode; try { @@ -153,16 +204,14 @@ public final class Picocli { } } - private static int runReAugmentationIfNeeded(List cliArgs, CommandLine cmd) { + private static int runReAugmentationIfNeeded(List cliArgs, CommandLine cmd, CommandLine currentCommand) { int exitCode = 0; - CommandLine currentCommandSpec = getCurrentCommandSpec(cliArgs, cmd.getCommandSpec()); - - if (currentCommandSpec == null) { + if (currentCommand == null) { return exitCode; // possible if using --version or the user made a mistake } - String currentCommandName = currentCommandSpec.getCommandName(); + String currentCommandName = currentCommand.getCommandName(); if (shouldSkipRebuild(cliArgs, currentCommandName)) { return exitCode; @@ -176,7 +225,7 @@ public final class Picocli { Environment.forceDevProfile(); } } - if (requiresReAugmentation(currentCommandSpec)) { + if (requiresReAugmentation(currentCommand)) { PropertyMappers.sanitizeDisabledMappers(); exitCode = runReAugmentation(cliArgs, cmd); } @@ -190,6 +239,7 @@ public final class Picocli { || cliArgs.contains("--help-all") || currentCommandName.equals(Build.NAME) || currentCommandName.equals(ShowConfig.NAME) + || currentCommandName.equals(BootstrapAdmin.NAME) || currentCommandName.equals(Tools.NAME); } @@ -246,15 +296,19 @@ public final class Picocli { checkChangesInBuildOptionsDuringAutoBuild(); } - int exitCode; + List configArgsList = new ArrayList<>(); + configArgsList.add(Build.NAME); + parseConfigArgs(cliArgs, (k, v) -> { + PropertyMapper mapper = PropertyMappers.getMapper(k); - List configArgsList = new ArrayList<>(cliArgs); + if (mapper == null || mapper.isRunTime()) { + return; + } - String commandName = getCurrentCommandSpec(cliArgs, cmd.getCommandSpec()).getCommandName(); - configArgsList.replaceAll(arg -> replaceCommandWithBuild(commandName, arg)); - configArgsList.removeIf(Picocli::isRuntimeOption); + configArgsList.add(k + "=" + v); + }, ignored -> {}); - exitCode = cmd.execute(configArgsList.toArray(new String[0])); + int exitCode = cmd.execute(configArgsList.toArray(new String[0])); if(!isDevMode() && exitCode == cmd.getCommandSpec().exitCodeOnSuccess()) { cmd.getOut().printf("Next time you run the server, just run:%n%n\t%s %s %s%n%n", Environment.getCommand(), String.join(" ", getSanitizedRuntimeCliOptions()), OPTIMIZED_BUILD_OPTION_LONG); @@ -589,25 +643,9 @@ public final class Picocli { return key.startsWith("kc.provider.file"); } - public static CommandLine createCommandLine(List cliArgs) { + public static CommandLine createCommandLine(Consumer consumer) { CommandSpec spec = CommandSpec.forAnnotatedObject(new Main()).name(Environment.getCommand()); - - for (CommandLine subCommand : spec.subcommands().values()) { - CommandSpec subCommandSpec = subCommand.getCommandSpec(); - - // help option added to any subcommand - subCommandSpec.addOption(OptionSpec.builder(Help.OPTION_NAMES) - .usageHelp(true) - .description("This help message.") - .build()); - } - - addCommandOptions(cliArgs, getCurrentCommandSpec(cliArgs, spec)); - - if (isRebuildCheck()) { - // build command should be available when running re-aug - addCommandOptions(cliArgs, spec.subcommands().get(Build.NAME)); - } + consumer.accept(spec); CommandLine cmd = new CommandLine(spec); @@ -620,6 +658,17 @@ public final class Picocli { return cmd; } + private static void addHelp(CommandSpec currentSpec) { + try { + currentSpec.addOption(OptionSpec.builder(Help.OPTION_NAMES) + .usageHelp(true) + .description("This help message.") + .build()); + } catch (DuplicateOptionAnnotationsException e) { + // Completion is inheriting mixinStandardHelpOptions = true + } + } + private static IncludeOptions getIncludeOptions(List cliArgs, AbstractCommand abstractCommand, String commandName) { IncludeOptions result = new IncludeOptions(); if (abstractCommand == null) { @@ -653,18 +702,6 @@ public final class Picocli { } } - private static CommandLine getCurrentCommandSpec(List cliArgs, CommandSpec spec) { - for (String arg : cliArgs) { - CommandLine command = spec.subcommands().get(arg); - - if (command != null) { - return command; - } - } - - return null; - } - private static void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) { final Map>> mappers = new EnumMap<>(OptionCategory.class); @@ -850,13 +887,6 @@ public final class Picocli { return args; } - private static String replaceCommandWithBuild(String commandName, String arg) { - if (arg.equals(commandName)) { - return Build.NAME; - } - return arg; - } - private static boolean isRuntimeOption(String arg) { return arg.startsWith(ImportRealmMixin.IMPORT_REALM); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java index 8c7cd3bbed0..c93e99a932a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java @@ -73,4 +73,5 @@ public abstract class AbstractCommand { public CommandLine getCommandLine() { return spec.commandLine(); } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractExportImportCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java similarity index 84% rename from quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractExportImportCommand.java rename to quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java index e7d0418d539..dcdf7d43418 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractExportImportCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java @@ -19,14 +19,14 @@ package org.keycloak.quarkus.runtime.cli.command; import org.keycloak.config.OptionCategory; import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication; + import picocli.CommandLine; import java.util.List; import java.util.stream.Collectors; -public abstract class AbstractExportImportCommand extends AbstractStartCommand implements Runnable { - - private final String action; +public abstract class AbstractNonServerCommand extends AbstractStartCommand implements Runnable { @CommandLine.Mixin OptimizedMixin optimizedMixin; @@ -34,15 +34,9 @@ public abstract class AbstractExportImportCommand extends AbstractStartCommand i @CommandLine.Mixin HelpAllMixin helpAllMixin; - protected AbstractExportImportCommand(String action) { - this.action = action; - } - @Override public void run() { - System.setProperty("keycloak.migration.action", action); - - Environment.setProfile(Environment.IMPORT_EXPORT_MODE); + Environment.setProfile(Environment.NON_SERVER_MODE); super.run(); } @@ -64,4 +58,7 @@ public abstract class AbstractExportImportCommand extends AbstractStartCommand i public boolean includeRuntime() { return true; } + + public void onStart(QuarkusKeycloakApplication application) { + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java index 3e31fa15340..da0ba1766ad 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java @@ -17,10 +17,16 @@ package org.keycloak.quarkus.runtime.cli.command; +import org.keycloak.config.OptionCategory; +import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.KeycloakMain; import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler; import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; + import picocli.CommandLine; public abstract class AbstractStartCommand extends AbstractCommand implements Runnable { @@ -28,6 +34,7 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru @Override public void run() { + Environment.setParsedCommand(this); doBeforeRun(); CommandLine cmd = spec.commandLine(); HttpPropertyMappers.validateConfig(); @@ -38,4 +45,15 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru protected void doBeforeRun() { } + + @Override + public List getOptionCategories() { + EnumSet excludedCategories = excludedCategories(); + return super.getOptionCategories().stream().filter(optionCategory -> !excludedCategories.contains(optionCategory)).collect(Collectors.toList()); + } + + protected EnumSet excludedCategories() { + return EnumSet.of(OptionCategory.IMPORT, OptionCategory.EXPORT); + } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdmin.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdmin.java new file mode 100644 index 00000000000..5c0caeab415 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdmin.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.cli.command; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.ScopeType; + +@Command(name = BootstrapAdmin.NAME, header = BootstrapAdmin.HEADER, description = "%n" + + BootstrapAdmin.HEADER, subcommands = {BootstrapAdminUser.class, BootstrapAdminService.class}) +public class BootstrapAdmin { + + public static final String NAME = "bootstrap-admin"; + public static final String HEADER = "Commands for bootstrapping admin access"; + public static final String KEYCLOAK_BOOTSTRAP_ADMIN_EXPIRATION_ENV_VAR = "KEYCLOAK_BOOTSTRAP_ADMIN_EXPIRATION"; + + @Option(names = { "--no-prompt" }, description = "Run non-interactive without prompting", scope = ScopeType.INHERIT) + boolean noPrompt; + + /*@Option(names = { + "--expiration" }, description = "Specifies the number of minutes after which the account expires. Defaults to " + + ApplianceBootstrap.DEFAULT_TEMP_ADMIN_EXPIRATION + ", or to the value of the " + + KEYCLOAK_BOOTSTRAP_ADMIN_EXPIRATION_ENV_VAR + " env variable if set", defaultValue = "${env:" + + KEYCLOAK_BOOTSTRAP_ADMIN_EXPIRATION_ENV_VAR + "}", scope = ScopeType.INHERIT) + */Integer expiration; + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdminService.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdminService.java new file mode 100644 index 00000000000..1cede5f9010 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdminService.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.cli.command; + +import org.keycloak.common.util.IoUtils; +import org.keycloak.quarkus.runtime.cli.PropertyException; +import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication; +import org.keycloak.services.managers.ApplianceBootstrap; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = BootstrapAdminService.NAME, header = BootstrapAdminService.HEADER, description = "%n" + + BootstrapAdminService.HEADER) +public class BootstrapAdminService extends AbstractNonServerCommand { + + public static final String NAME = "service"; + public static final String HEADER = "Add an admin service account"; + + static class ClientIdOptions { + @Option(names = { "--client-id" }, description = "Client id, defaults to " + + ApplianceBootstrap.DEFAULT_TEMP_ADMIN_SERVICE) + String clientId; + + @Option(names = { "--client-id:env" }, description = "Environment variable name for the client id") + String cliendIdEnv; + } + + @ArgGroup(exclusive = true, multiplicity = "0..1") + ClientIdOptions clientIdOptions; + + @Option(names = { "--client-secret:env" }, description = "Environment variable name for the client secret") + String clientSecretEnv; + + String clientSecret; + String clientId; + + @Override + public String getName() { + return NAME; + } + + @Override + protected void doBeforeRun() { + BootstrapAdmin bootstrap = spec.commandLine().getParent().getCommand(); + if (clientIdOptions != null) { + if (clientIdOptions.cliendIdEnv != null) { + clientId = getFromEnv(clientIdOptions.cliendIdEnv); + } else { + clientId = clientIdOptions.clientId; + } + } else if (!bootstrap.noPrompt) { + clientId = IoUtils.readLineFromConsole("client id", ApplianceBootstrap.DEFAULT_TEMP_ADMIN_SERVICE); + } + + if (clientSecretEnv == null) { + if (bootstrap.noPrompt) { + throw new PropertyException("No client secret provided"); + } + clientSecret = IoUtils.readPasswordFromConsole("client secret"); + String confirmClientSecret = IoUtils.readPasswordFromConsole("client secret again"); + if (!clientSecret.equals(confirmClientSecret)) { + throw new PropertyException("Client secrets do not match"); + } + } else { + clientSecret = getFromEnv(clientSecretEnv); + } + } + + private String getFromEnv(String envVar) { + String result = System.getenv(envVar); + if (result == null) { + throw new PropertyException(String.format("Environment variable %s not found", envVar)); + } + return result; + } + + @Override + public void onStart(QuarkusKeycloakApplication application) { + //BootstrapAdmin bootstrap = spec.commandLine().getParent().getCommand(); + application.createTemporaryMasterRealmAdminService(clientId, clientSecret, /*bootstrap.expiration,*/ null); + } + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdminUser.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdminUser.java new file mode 100644 index 00000000000..04177a37b13 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/BootstrapAdminUser.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.cli.command; + +import org.keycloak.common.util.IoUtils; +import org.keycloak.quarkus.runtime.cli.PropertyException; +import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication; +import org.keycloak.services.managers.ApplianceBootstrap; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = BootstrapAdminUser.NAME, header = BootstrapAdminUser.HEADER, description = "%n" + + BootstrapAdminUser.HEADER) +public class BootstrapAdminUser extends AbstractNonServerCommand { + + public static final String NAME = "user"; + public static final String HEADER = "Add an admin user with a password"; + + static class UsernameOptions { + @Option(names = { "--username" }, description = "Username of admin user, defaults to " + + ApplianceBootstrap.DEFAULT_TEMP_ADMIN_USERNAME) + String username; + + @Option(names = { "--username:env" }, description = "Environment variable name for the admin username") + String usernameEnv; + } + + @ArgGroup(exclusive = true, multiplicity = "0..1") + UsernameOptions usernameOptions; + + @Option(names = { "--password:env" }, description = "Environment variable name for the admin user password") + String passwordEnv; + + String password; + String username; + + @Override + public String getName() { + return NAME; + } + + @Override + protected void doBeforeRun() { + BootstrapAdmin bootstrap = spec.commandLine().getParent().getCommand(); + if (usernameOptions != null) { + if (usernameOptions.usernameEnv != null) { + username = getFromEnv(usernameOptions.usernameEnv); + } else { + username = usernameOptions.username; + } + } else if (!bootstrap.noPrompt) { + username = IoUtils.readLineFromConsole("username", ApplianceBootstrap.DEFAULT_TEMP_ADMIN_USERNAME); + } + + if (passwordEnv == null) { + if (bootstrap.noPrompt) { + throw new PropertyException("No password provided"); + } + password = IoUtils.readPasswordFromConsole("password"); + String confirmPassword = IoUtils.readPasswordFromConsole("password again"); + if (!password.equals(confirmPassword)) { + throw new PropertyException("Passwords do not match"); + } + } else { + password = getFromEnv(passwordEnv); + } + } + + private String getFromEnv(String envVar) { + String result = System.getenv(envVar); + if (result == null) { + throw new PropertyException(String.format("Environment variable %s not found", envVar)); + } + return result; + } + + @Override + public void onStart(QuarkusKeycloakApplication application) { + //BootstrapAdmin bootstrap = spec.commandLine().getParent().getCommand(); + application.createTemporaryMasterRealmAdminUser(username, password, /*bootstrap.expiration,*/ null); + } + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java index 878cf6c2ee2..9bf397c9b4a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java @@ -20,27 +20,22 @@ package org.keycloak.quarkus.runtime.cli.command; import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT; import org.keycloak.config.OptionCategory; +import org.keycloak.exportimport.ExportImportConfig; import org.keycloak.quarkus.runtime.configuration.mappers.ExportPropertyMappers; import picocli.CommandLine.Command; -import java.util.List; -import java.util.stream.Collectors; +import java.util.EnumSet; @Command(name = Export.NAME, header = "Export data from realms to a file or directory.", description = "%nExport data from realms to a file or directory.") -public final class Export extends AbstractExportImportCommand implements Runnable { +public final class Export extends AbstractNonServerCommand implements Runnable { public static final String NAME = "export"; - public Export() { - super(ACTION_EXPORT); - } - @Override - public List getOptionCategories() { - return super.getOptionCategories().stream().filter(optionCategory -> - optionCategory != OptionCategory.IMPORT).collect(Collectors.toList()); + protected void doBeforeRun() { + System.setProperty(ExportImportConfig.ACTION, ACTION_EXPORT); } @Override @@ -54,4 +49,9 @@ public final class Export extends AbstractExportImportCommand implements Runnabl return NAME; } + @Override + protected EnumSet excludedCategories() { + return EnumSet.of(OptionCategory.IMPORT); + } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/HelpAllMixin.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/HelpAllMixin.java index 49ade4d66eb..40c30750a05 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/HelpAllMixin.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/HelpAllMixin.java @@ -30,7 +30,6 @@ public final class HelpAllMixin { @CommandLine.Option(names = {HELP_ALL_OPTION}, usageHelp = true, description = "This same help message but with additional options.") public void setHelpAll(boolean allOptions) { - Help help = (Help) spec.commandLine().getHelp(); - help.setAllOptions(true); + Help.setAllOptions(true); } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java index aa0b64022fc..7032f3159d8 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java @@ -20,27 +20,22 @@ package org.keycloak.quarkus.runtime.cli.command; import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT; import org.keycloak.config.OptionCategory; +import org.keycloak.exportimport.ExportImportConfig; import org.keycloak.quarkus.runtime.configuration.mappers.ImportPropertyMappers; import picocli.CommandLine.Command; -import java.util.List; -import java.util.stream.Collectors; +import java.util.EnumSet; @Command(name = Import.NAME, header = "Import data from a directory or a file.", description = "%nImport data from a directory or a file.") -public final class Import extends AbstractExportImportCommand implements Runnable { +public final class Import extends AbstractNonServerCommand implements Runnable { public static final String NAME = "import"; - public Import() { - super(ACTION_IMPORT); - } - @Override - public List getOptionCategories() { - return super.getOptionCategories().stream().filter(optionCategory -> - optionCategory != OptionCategory.EXPORT).collect(Collectors.toList()); + protected void doBeforeRun() { + System.setProperty(ExportImportConfig.ACTION, ACTION_IMPORT); } @Override @@ -54,4 +49,9 @@ public final class Import extends AbstractExportImportCommand implements Runnabl return NAME; } + @Override + protected EnumSet excludedCategories() { + return EnumSet.of(OptionCategory.EXPORT); + } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java index 76556aa4bc6..861f9dff0ab 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java @@ -66,7 +66,8 @@ import java.nio.file.Path; Export.class, Import.class, ShowConfig.class, - Tools.class + Tools.class, + BootstrapAdmin.class }) public final class Main { @@ -78,11 +79,6 @@ public final class Main { @CommandLine.Spec CommandLine.Model.CommandSpec spec; - @Option(names = { "-h", "--help" }, - description = "This help message.", - usageHelp = true) - boolean help; - @Option(names = { "-V", "--version" }, description = "Show version information", versionHelp = true) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java index 7effd9b10a0..f30191ef642 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java @@ -21,16 +21,13 @@ import static org.keycloak.quarkus.runtime.Environment.setProfile; import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG; import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty; -import org.keycloak.config.OptionCategory; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Messages; import picocli.CommandLine; import picocli.CommandLine.Command; -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Command(name = Start.NAME, header = "Start the server.", @@ -73,11 +70,6 @@ public final class Start extends AbstractStartCommand implements Runnable { return Environment.isDevProfile(); } - @Override - public List getOptionCategories() { - return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList()); - } - @Override public boolean includeRuntime() { return true; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java index c9f42c8dce3..60af84ae1da 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java @@ -17,16 +17,12 @@ package org.keycloak.quarkus.runtime.cli.command; -import org.keycloak.config.OptionCategory; import org.keycloak.quarkus.runtime.Environment; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; -import java.util.List; -import java.util.stream.Collectors; - @Command(name = StartDev.NAME, header = "Start the server in development mode.", description = { @@ -49,11 +45,6 @@ public final class StartDev extends AbstractStartCommand implements Runnable { Environment.forceDevProfile(); } - @Override - public List getOptionCategories() { - return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList()); - } - @Override public boolean includeRuntime() { return true; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/BootstrapAdminPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/BootstrapAdminPropertyMappers.java new file mode 100644 index 00000000000..8fa0aef5dcd --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/BootstrapAdminPropertyMappers.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.configuration.mappers; + +import org.keycloak.config.BootstrapAdminOptions; + +import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; + +public final class BootstrapAdminPropertyMappers { + + private static final String PASSWORD_SET = "bootstrap admin password is set"; + private static final String CLIENT_SECRET_SET = "bootstrap admin client secret is set"; + + private BootstrapAdminPropertyMappers() { + } + + public static PropertyMapper[] getMappers() { + return new PropertyMapper[]{ + fromOption(BootstrapAdminOptions.USERNAME) + .paramLabel("username") + .isEnabled(BootstrapAdminPropertyMappers::isPasswordSet, PASSWORD_SET) + .build(), + fromOption(BootstrapAdminOptions.PASSWORD) + .paramLabel("password") + .build(), + fromOption(BootstrapAdminOptions.EXPIRATION) + .paramLabel("expiration") + .isEnabled(BootstrapAdminPropertyMappers::isPasswordSet, PASSWORD_SET) + .build(), + fromOption(BootstrapAdminOptions.CLIENT_ID) + .paramLabel("client id") + .isEnabled(BootstrapAdminPropertyMappers::isClientSecretSet, CLIENT_SECRET_SET) + .build(), + fromOption(BootstrapAdminOptions.CLIENT_SECRET) + .paramLabel("client secret") + .build(), + }; + } + + private static boolean isPasswordSet() { + return getOptionalKcValue(BootstrapAdminOptions.PASSWORD.getKey()).isPresent(); + } + + private static boolean isClientSecretSet() { + return getOptionalKcValue(BootstrapAdminOptions.CLIENT_SECRET.getKey()).isPresent(); + } + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java index 8ed51eaeec4..944d033fe60 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java @@ -153,7 +153,7 @@ public final class HttpPropertyMappers { boolean enabled = Boolean.parseBoolean(value.get()); Optional proxy = Configuration.getOptionalKcValue("proxy"); - if (Environment.isDevMode() || Environment.isImportExportMode() + if (Environment.isDevMode() || Environment.isNonServerMode() || ("edge".equalsIgnoreCase(proxy.orElse("")))) { enabled = true; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java index 8e59b823f58..04a6a0db43a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java @@ -22,20 +22,25 @@ import io.quarkus.runtime.StartupEvent; import io.smallrye.common.annotation.Blocking; import org.keycloak.Config; +import org.keycloak.config.BootstrapAdminOptions; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.platform.Platform; +import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.cli.command.AbstractNonServerCommand; +import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; import org.keycloak.quarkus.runtime.integration.QuarkusPlatform; import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.resources.KeycloakApplication; +import org.keycloak.utils.StringUtil; import jakarta.enterprise.event.Observes; import jakarta.ws.rs.ApplicationPath; -import static org.keycloak.quarkus.runtime.Environment.isImportExportMode; - @ApplicationPath("/") @Blocking public class QuarkusKeycloakApplication extends KeycloakApplication { @@ -49,9 +54,11 @@ public class QuarkusKeycloakApplication extends KeycloakApplication { QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform(); platform.started(); startup(); - if (!isImportExportMode()) { - createAdminUser(); - } + Environment.getParsedCommand().ifPresent(ac -> { + if (ac instanceof AbstractNonServerCommand) { + ((AbstractNonServerCommand)ac).onStart(this); + } + }); } void onShutdownEvent(@Observes ShutdownEvent event) { @@ -70,21 +77,51 @@ public class QuarkusKeycloakApplication extends KeycloakApplication { // no need to load config provider because we force quarkus impl } - private void createAdminUser() { - String adminUserName = getEnvOrProp(KEYCLOAK_ADMIN_ENV_VAR, KEYCLOAK_ADMIN_PROP_VAR); - String adminPassword = getEnvOrProp(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR, KEYCLOAK_ADMIN_PASSWORD_PROP_VAR); + @Override + protected void createTemporaryAdmin(KeycloakSession session) { + var adminUsername = Configuration.getOptionalKcValue(BootstrapAdminOptions.USERNAME.getKey()).orElse(getEnvOrProp(KEYCLOAK_ADMIN_ENV_VAR, KEYCLOAK_ADMIN_PROP_VAR)); + var adminPassword = Configuration.getOptionalKcValue(BootstrapAdminOptions.PASSWORD.getKey()).orElse(getEnvOrProp(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR, KEYCLOAK_ADMIN_PASSWORD_PROP_VAR)); - if ((adminUserName == null || adminUserName.trim().length() == 0) - || (adminPassword == null || adminPassword.trim().length() == 0)) { - return; - } - - KeycloakSessionFactory sessionFactory = KeycloakApplication.getSessionFactory(); + var clientId = Configuration.getOptionalKcValue(BootstrapAdminOptions.CLIENT_ID.getKey()).orElse(null); + var clientSecret = Configuration.getOptionalKcValue(BootstrapAdminOptions.CLIENT_SECRET.getKey()).orElse(null); try { - KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> { - new ApplianceBootstrap(session).createMasterRealmUser(adminUserName, adminPassword); - }); + //Integer expiration = Configuration.getOptionalKcValue(BootstrapAdminOptions.EXPIRATION.getKey()).map(Integer::valueOf).orElse(null); + if (StringUtil.isNotBlank(adminPassword)) { + createTemporaryMasterRealmAdminUser(adminUsername, adminPassword, /*expiration,*/ session); + } + if (StringUtil.isNotBlank(clientSecret)) { + createTemporaryMasterRealmAdminService(clientId, clientSecret, /*expiration,*/ session); + } + } catch (NumberFormatException e) { + throw new RuntimeException("Invalid admin expiration value provided. An integer is expected.", e); + } + } + + public void createTemporaryMasterRealmAdminUser(String adminUserName, String adminPassword, /*Integer adminExpiration,*/ KeycloakSession existingSession) { + KeycloakSessionTask task = session -> { + new ApplianceBootstrap(session).createTemporaryMasterRealmAdminUser(adminUserName, adminPassword /*, adminExpiration*/, false); + }; + + runAdminTask(adminUserName, existingSession, task); + } + + public void createTemporaryMasterRealmAdminService(String clientId, String clientSecret, /*Integer adminExpiration,*/ KeycloakSession existingSession) { + KeycloakSessionTask task = session -> { + new ApplianceBootstrap(session).createTemporaryMasterRealmAdminService(clientId, clientSecret /*, adminExpiration*/); + }; + + runAdminTask(clientId, existingSession, task); + } + + private void runAdminTask(String adminUserName, KeycloakSession existingSession, KeycloakSessionTask task) { + try { + if (existingSession == null) { + KeycloakSessionFactory sessionFactory = KeycloakApplication.getSessionFactory(); + KeycloakModelUtils.runJobInTransaction(sessionFactory, task); + } else { + task.run(existingSession); + } } catch (Throwable t) { ServicesLogger.LOGGER.addUserFailed(t, adminUserName, Config.getAdminRealm()); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/database/QuarkusJpaConnectionProviderFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/database/QuarkusJpaConnectionProviderFactory.java index 6d9fdbf4d34..c459f4eb0ff 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/database/QuarkusJpaConnectionProviderFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/database/QuarkusJpaConnectionProviderFactory.java @@ -127,7 +127,7 @@ public class QuarkusJpaConnectionProviderFactory extends AbstractJpaConnectionPr throw new RuntimeException("Failed to update database.", cause); } - if (schemaChanged || Environment.isImportExportMode()) { + if (schemaChanged || Environment.isNonServerMode()) { runJobInTransaction(factory, this::initSchema); } else { Version.RESOURCES_VERSION = id; diff --git a/quarkus/runtime/src/main/resources/META-INF/keycloak.conf b/quarkus/runtime/src/main/resources/META-INF/keycloak.conf index 7df84f6363e..29cdfd88115 100644 --- a/quarkus/runtime/src/main/resources/META-INF/keycloak.conf +++ b/quarkus/runtime/src/main/resources/META-INF/keycloak.conf @@ -11,11 +11,11 @@ db=dev-file %dev.spi-theme-static-max-age=-1 # The default configuration when running in import or export mode -%import_export.http-enabled=true -%import_export.http-server-enabled=false -%import_export.hostname-strict=false -%import_export.hostname-strict-https=false -%import_export.cache=local +%nonserver.http-enabled=true +%nonserver.http-server-enabled=false +%nonserver.hostname-strict=false +%nonserver.hostname-strict-https=false +%nonserver.cache=local #logging defaults log-console-output=default diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java index 94df7eb84c0..904588073bf 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java @@ -595,7 +595,7 @@ public class ConfigurationTest { @Test public void testResolvePropertyFromDefaultProfile() { - Environment.setProfile("import_export"); + Environment.setProfile(Environment.NON_SERVER_MODE); assertEquals("false", createConfig().getConfigValue("kc.hostname-strict").getValue()); Environment.setProfile("prod"); diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java index 1058ab815c6..5139014e9db 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java @@ -20,7 +20,9 @@ package org.keycloak.it.cli; import org.junit.jupiter.api.Test; import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.CLITest; +import org.keycloak.it.junit5.extension.ConfigurationTestResource; +import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.main.Launch; import io.quarkus.test.junit.main.LaunchResult; import org.keycloak.it.utils.KeycloakDistribution; @@ -29,6 +31,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; +@QuarkusTestResource(value = ConfigurationTestResource.class, restrictToAnnotatedClass = true) @CLITest public class OptionValidationTest { @@ -40,7 +43,7 @@ public class OptionValidationTest { } @Test - @Launch({"build", "--db", "foo", "bar"}) + @Launch({"build", "--db", "mysql", "postgres"}) public void failMultipleOptionValue(LaunchResult result) { CLIResult cliResult = (CLIResult) result; assertThat(cliResult.getErrorOutput(), containsString("Option '--db' (vendor) expects a single value. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres")); diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BootstrapAdminDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BootstrapAdminDistTest.java new file mode 100644 index 00000000000..dceea055b77 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BootstrapAdminDistTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.it.cli.dist; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.RawDistOnly; +import org.keycloak.it.junit5.extension.WithEnvVars; +import org.keycloak.it.utils.KeycloakDistribution; +import org.keycloak.it.utils.RawKeycloakDistribution; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DistributionTest +@RawDistOnly(reason = "Containers are immutable") +public class BootstrapAdminDistTest { + + @Test + @Launch({ "bootstrap-admin", "user", "--no-prompt" }) + void failNoPassword(LaunchResult result) { + assertTrue(result.getErrorOutput().contains("No password provided"), + () -> "The Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); + } + + /** + @Test + @Launch({ "bootstrap-admin", "user", "--expiration=tomorrow" }) + void failBadExpiration(LaunchResult result) { + assertTrue(result.getErrorOutput().contains("Invalid value for option '--expiration': 'tomorrow' is not an int"), + () -> "The Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); + }*/ + + @Test + @Launch({ "bootstrap-admin", "user", "--username=admin", "--password:env=MY_PASSWORD" }) + void failEnvNotSet(LaunchResult result) { + assertTrue(result.getErrorOutput().contains("Environment variable MY_PASSWORD not found"), + () -> "The Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); + } + + @Test + @WithEnvVars({"MY_PASSWORD", "admin123"}) + @Launch({ "bootstrap-admin", "user", "--username=admin", "--password:env=MY_PASSWORD" }) + void createAdmin(LaunchResult result) { + assertTrue(result.getErrorOutput().isEmpty(), result.getErrorOutput()); + } + + @Test + @Launch({ "bootstrap-admin", "service", "--no-prompt" }) + void failServiceAccountNoSecret(LaunchResult result) { + assertTrue(result.getErrorOutput().contains("No client secret provided"), + () -> "The Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); + } + + @Test + @Launch({ "bootstrap-admin", "service", "--client-id=admin", "--client-secret:env=MY_SECRET" }) + void failServiceAccountEnvNotSet(LaunchResult result) { + assertTrue(result.getErrorOutput().contains("Environment variable MY_SECRET not found"), + () -> "The Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); + } + + @Test + @WithEnvVars({"MY_SECRET", "admin123"}) + void createAndUseSericeAccountAdmin(KeycloakDistribution dist) throws Exception { + RawKeycloakDistribution rawDist = dist.unwrap(RawKeycloakDistribution.class); + CLIResult result = rawDist.run("bootstrap-admin", "service", "--client-id=admin", "--client-secret:env=MY_SECRET"); + + assertTrue(result.getErrorOutput().isEmpty(), result.getErrorOutput()); + + rawDist.setManualStop(true); + rawDist.run("start-dev", "--log-level=debug"); + + CLIResult adminResult = rawDist.kcadm("get", "clients", "--server", "http://localhost:8080", "--realm", "master", "--client", "admin", "--secret", "admin123"); + + assertEquals(0, adminResult.exitCode()); + assertTrue(adminResult.getOutput().contains("clientId")); + } + +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java index 462e9bab640..8a2c5c3fcbe 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java @@ -83,14 +83,14 @@ public class BuildAndStartDistTest { } private void assertAdminCreation(KeycloakDistribution dist, LaunchResult result, String initialUsername, String nextUsername, String password) { - assertTrue(result.getOutput().contains("Added user '" + initialUsername + "' to realm 'master'"), + assertTrue(result.getOutput().contains("Created temporary admin user with username " + initialUsername), () -> "The Output:\n" + result.getOutput() + "doesn't contains the expected string."); dist.setEnvVar("KEYCLOAK_ADMIN", nextUsername); dist.setEnvVar("KEYCLOAK_ADMIN_PASSWORD", password); CLIResult cliResult = dist.run("start-dev", "--log-level=org.keycloak.services:debug"); - cliResult.assertMessage("Skipping create admin user. Admin already exists in realm 'master'."); + cliResult.assertNoMessage("Added temporary admin user '"); cliResult.assertStartedDevMode(); } } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FipsDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FipsDistTest.java index b7026df5bde..d2630e935d3 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FipsDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FipsDistTest.java @@ -33,6 +33,8 @@ import io.quarkus.test.junit.main.LaunchResult; @RawDistOnly(reason = "Containers are immutable") public class FipsDistTest { + private static final String BCFIPS_VERSION = "BCFIPS version 1.000205"; + @Test void testFipsNonApprovedMode(KeycloakDistribution dist) { runOnFipsEnabledDistribution(dist, () -> { @@ -41,12 +43,12 @@ public class FipsDistTest { // Not shown as FIPS is not a preview anymore cliResult.assertMessageWasShownExactlyNumberOfTimes("Preview features enabled: fips:v1", 0); cliResult.assertMessage("Java security providers: [ \n" - + " KC(BCFIPS version 1.000205, FIPS-JVM: " + KeycloakFipsSecurityProvider.isSystemFipsEnabled() + ") version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider"); + + " KC(" + BCFIPS_VERSION + ", FIPS-JVM: " + KeycloakFipsSecurityProvider.isSystemFipsEnabled() + ") version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider"); }); } @Test - void testFipsApprovedMode(KeycloakDistribution dist) { + void testFipsApprovedModePasswordFails(KeycloakDistribution dist) { runOnFipsEnabledDistribution(dist, () -> { dist.setEnvVar("KEYCLOAK_ADMIN", "admin"); dist.setEnvVar("KEYCLOAK_ADMIN_PASSWORD", "admin"); @@ -56,12 +58,21 @@ public class FipsDistTest { cliResult.assertMessage( "org.bouncycastle.crypto.fips.FipsUnapprovedOperationError: password must be at least 112 bits"); cliResult.assertMessage("Java security providers: [ \n" - + " KC(BCFIPS version 1.000205 Approved Mode, FIPS-JVM: " + KeycloakFipsSecurityProvider.isSystemFipsEnabled() + ") version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider"); + + " KC(" + BCFIPS_VERSION + " Approved Mode, FIPS-JVM: " + KeycloakFipsSecurityProvider.isSystemFipsEnabled() + ") version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider"); + }); + } + @Test + void testFipsApprovedModePasswordSucceeds(KeycloakDistribution dist) { + runOnFipsEnabledDistribution(dist, () -> { + dist.setEnvVar("KEYCLOAK_ADMIN", "admin"); dist.setEnvVar("KEYCLOAK_ADMIN_PASSWORD", "adminadminadmin"); - cliResult = dist.run("start", "--fips-mode=strict"); + + CLIResult cliResult = dist.run("start", "--fips-mode=strict"); cliResult.assertStarted(); - cliResult.assertMessage("Added user 'admin' to realm 'master'"); + cliResult.assertMessage("Java security providers: [ \n" + + " KC(" + BCFIPS_VERSION + " Approved Mode, FIPS-JVM: " + KeycloakFipsSecurityProvider.isSystemFipsEnabled() + ") version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider"); + cliResult.assertMessage("Created temporary admin user with username admin"); }); } diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testDefaultToHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testDefaultToHelp.approved.txt index 87f19f1682b..6ded000f090 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testDefaultToHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testDefaultToHelp.approved.txt @@ -27,6 +27,9 @@ Commands: show-config Print out the current configuration. tools Utilities for use and interaction with the server. completion Generate bash/zsh completion script for kc.sh. + bootstrap-admin Commands for bootstrapping admin access + user Add an admin user with a password + service Add an admin service account Examples: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelp.approved.txt index 87f19f1682b..6ded000f090 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelp.approved.txt @@ -27,6 +27,9 @@ Commands: show-config Print out the current configuration. tools Utilities for use and interaction with the server. completion Generate bash/zsh completion script for kc.sh. + bootstrap-admin Commands for bootstrapping admin access + user Add an admin user with a password + service Add an admin service account Examples: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelpShort.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelpShort.approved.txt index 87f19f1682b..6ded000f090 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelpShort.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelpShort.approved.txt @@ -27,6 +27,9 @@ Commands: show-config Print out the current configuration. tools Utilities for use and interaction with the server. completion Generate bash/zsh completion script for kc.sh. + bootstrap-admin Commands for bootstrapping admin access + user Add an admin user with a password + service Add an admin service account Examples: diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java index 7ae757713db..6a9eb03d3d5 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java @@ -34,6 +34,7 @@ import org.keycloak.it.utils.RawKeycloakDistribution; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.command.Start; import org.keycloak.quarkus.runtime.cli.command.StartDev; +import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.KeycloakPropertiesConfigSource; import org.keycloak.quarkus.runtime.configuration.test.TestConfigArgsConfigSource; import org.keycloak.quarkus.runtime.integration.QuarkusPlatform; @@ -64,7 +65,6 @@ public class CLITestExtension extends QuarkusMainTestExtension { private DatabaseContainer databaseContainer; private InfinispanContainer infinispanContainer; private CLIResult result; - static String[] CLI_ARGS = new String[0]; @Override public void beforeEach(ExtensionContext context) throws Exception { @@ -115,7 +115,7 @@ public class CLITestExtension extends QuarkusMainTestExtension { result = dist.run(Stream.concat(List.of(launch.value()).stream(), List.of(distConfig.defaultOptions()).stream()).collect(Collectors.toList())); } } else { - CLI_ARGS = launch == null ? new String[] {} : launch.value(); + ConfigArgsConfigSource.setCliArgs(launch == null ? new String[] {} : launch.value()); configureProfile(context); super.beforeEach(context); } diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/ConfigurationTestResource.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/ConfigurationTestResource.java index 19cb678a6d0..5880931d754 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/ConfigurationTestResource.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/ConfigurationTestResource.java @@ -24,7 +24,6 @@ import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigProviderResolver; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider; import java.util.Map; @@ -46,7 +45,6 @@ public class ConfigurationTestResource implements QuarkusTestResourceLifecycleMa @Override public void inject(Object testInstance) { - ConfigArgsConfigSource.setCliArgs(CLITestExtension.CLI_ARGS); KeycloakConfigSourceProvider.reload(); SmallRyeConfig config = ConfigUtils.configBuilder(true, LaunchMode.NORMAL).build(); SmallRyeConfigProviderResolver resolver = new SmallRyeConfigProviderResolver(); diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java index bdbd38655cc..e924dfc0c21 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java @@ -8,7 +8,8 @@ import java.util.List; public interface KeycloakDistribution { String SCRIPT_CMD = Environment.isWindows() ? "kc.bat" : "kc.sh"; - String SCRIPT_CMD_INVOKABLE = Environment.isWindows() ? SCRIPT_CMD : "./"+SCRIPT_CMD; + + String SCRIPT_KCADM_CMD = Environment.isWindows() ? "kcadm.bat" : "kcadm.sh"; CLIResult run(List arguments); default CLIResult run(String... arguments) { diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java index 6f984c35d55..a6a0fb278c4 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java @@ -36,6 +36,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -110,6 +111,50 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { this.requestPort = requestPort; this.distPath = prepareDistribution(); } + + public CLIResult kcadm(String... arguments) throws IOException { + return kcadm(Arrays.asList(arguments)); + } + + public CLIResult kcadm(List arguments) throws IOException { + List allArgs = new ArrayList<>(); + + invoke(allArgs, SCRIPT_KCADM_CMD); + + if (this.isDebug()) { + allArgs.add("-x"); + } + + allArgs.addAll(arguments); + + ProcessBuilder pb = new ProcessBuilder(allArgs); + ProcessBuilder builder = pb.directory(distPath.resolve("bin").toFile()); + + // TODO: it is possible to debug kcadm, but it's more involved + /*if (debug) { + builder.environment().put("DEBUG_SUSPEND", "y"); + }*/ + + builder.environment().putAll(envVars); + + Process kcadm = builder.start(); + + List outputStream = new ArrayList<>(); + List errorStream = new ArrayList<>(); + readOutput(kcadm, outputStream, errorStream); + + int exitValue = kcadm.exitValue(); + + return CLIResult.create(outputStream, errorStream, exitValue); + } + + private void invoke(List allArgs, String cmd) { + if (isWindows()) { + allArgs.add(distPath.resolve("bin") + File.separator + cmd); + } else { + allArgs.add("./" + cmd); + } + } @Override public CLIResult run(List arguments) { @@ -237,11 +282,7 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { public String[] getCliArgs(List arguments) { List allArgs = new ArrayList<>(); - if (isWindows()) { - allArgs.add(distPath.resolve("bin") + File.separator + SCRIPT_CMD_INVOKABLE); - } else { - allArgs.add(SCRIPT_CMD_INVOKABLE); - } + invoke(allArgs, SCRIPT_CMD); if (this.isDebug()) { allArgs.add("--debug"); @@ -467,6 +508,9 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { if (!dPath.resolve("bin").resolve(SCRIPT_CMD).toFile().setExecutable(true)) { throw new RuntimeException("Cannot set " + SCRIPT_CMD + " executable"); } + if (!dPath.resolve("bin").resolve(SCRIPT_KCADM_CMD).toFile().setExecutable(true)) { + throw new RuntimeException("Cannot set " + SCRIPT_KCADM_CMD + " executable"); + } inited = true; @@ -477,11 +521,15 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { } private void readOutput() { + readOutput(keycloak, outputStream, errorStream); + } + + private static void readOutput(Process process, List outputStream, List errorStream) { try ( - BufferedReader outStream = new BufferedReader(new InputStreamReader(keycloak.getInputStream())); - BufferedReader errStream = new BufferedReader(new InputStreamReader(keycloak.getErrorStream())); + BufferedReader outStream = new BufferedReader(new InputStreamReader(process.getInputStream())); + BufferedReader errStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); ) { - while (keycloak.isAlive()) { + while (process.isAlive()) { readStream(outStream, outputStream); readStream(errStream, errorStream); // a hint to temporarily disable the current thread in favor of the process where the distribution is running @@ -493,7 +541,7 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { } } - private void readStream(BufferedReader reader, List stream) throws IOException { + private static void readStream(BufferedReader reader, List stream) throws IOException { String line; while (reader.ready() && (line = reader.readLine()) != null) { diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java index bcd544a878a..1bbe1cea557 100644 --- a/services/src/main/java/org/keycloak/services/ServicesLogger.java +++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java @@ -346,12 +346,12 @@ public interface ServicesLogger extends BasicLogger { void rejectedNonLocalAttemptToCreateInitialUser(String remoteAddr); @LogMessage(level = INFO) - @Message(id=77, value="Created initial admin user with username %s") - void createdInitialAdminUser(String userName); + @Message(id=77, value="Created temporary admin user with username %s") + void createdTemporaryAdminUser(String userName); - @LogMessage(level = WARN) - @Message(id=78, value="Rejected attempt to create initial user as user is already created") - void initialUserAlreadyCreated(); + @LogMessage(level = INFO) + @Message(id=78, value="Created temporary admin service account with client id %s") + void createdTemporaryAdminService(String clientId); @LogMessage(level = WARN) @Message(id=79, value="Locale not specified for messages.json") diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index 0498b973399..f8dbab5a9d9 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -20,6 +20,7 @@ import org.keycloak.Config; import org.keycloak.common.Version; import org.keycloak.common.enums.SslRequired; import org.keycloak.models.AdminRoles; +import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -27,11 +28,13 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.DefaultKeyProviders; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.userprofile.config.UPAttribute; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.services.ServicesLogger; import org.keycloak.userprofile.UserProfileProvider; +import org.keycloak.utils.StringUtil; /** * @author Bill Burke @@ -39,6 +42,10 @@ import org.keycloak.userprofile.UserProfileProvider; */ public class ApplianceBootstrap { + public static final String DEFAULT_TEMP_ADMIN_USERNAME = "temp-admin"; + public static final String DEFAULT_TEMP_ADMIN_SERVICE = "temp-admin-service"; + public static final int DEFAULT_TEMP_ADMIN_EXPIRATION = 120; + private final KeycloakSession session; public ApplianceBootstrap(KeycloakSession session) { @@ -106,17 +113,23 @@ public class ApplianceBootstrap { return true; } - public void createMasterRealmUser(String username, String password) { + public void createTemporaryMasterRealmAdminUser(String username, String password, /*Integer expriationMinutes,*/ boolean initialUser) { RealmModel realm = session.realms().getRealmByName(Config.getAdminRealm()); session.getContext().setRealm(realm); - if (session.users().getUsersCount(realm) > 0) { + username = StringUtil.isBlank(username) ? DEFAULT_TEMP_ADMIN_USERNAME : username; + //expriationMinutes = expriationMinutes == null ? DEFAULT_TEMP_ADMIN_EXPIRATION : expriationMinutes; + + if (initialUser && session.users().getUsersCount(realm) > 0) { ServicesLogger.LOGGER.addAdminUserFailedAdminExists(Config.getAdminRealm()); return; } UserModel adminUser = session.users().addUser(realm, username); adminUser.setEnabled(true); + // TODO: is this appropriate, does it need to be managed? + // adminUser.setSingleAttribute("temporary_admin", Boolean.TRUE.toString()); + // also set the expiration - could be relative to a creation timestamp, or computed UserCredentialModel usrCredModel = UserCredentialModel.password(password); adminUser.credentialManager().updateCredential(usrCredModel); @@ -124,7 +137,38 @@ public class ApplianceBootstrap { RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); adminUser.grantRole(adminRole); - ServicesLogger.LOGGER.addUserSuccess(username, Config.getAdminRealm()); + ServicesLogger.LOGGER.createdTemporaryAdminUser(username); + } + + public void createTemporaryMasterRealmAdminService(String clientId, String clientSecret /*, Integer expriationMinutes*/) { + RealmModel realm = session.realms().getRealmByName(Config.getAdminRealm()); + session.getContext().setRealm(realm); + + clientId = StringUtil.isBlank(clientId) ? DEFAULT_TEMP_ADMIN_SERVICE : clientId; + //expriationMinutes = expriationMinutes == null ? DEFAULT_TEMP_ADMIN_EXPIRATION : expriationMinutes; + + ClientRepresentation adminClient = new ClientRepresentation(); + adminClient.setClientId(clientId); + adminClient.setEnabled(true); + adminClient.setServiceAccountsEnabled(true); + adminClient.setPublicClient(false); + adminClient.setSecret(clientSecret); + + ClientModel adminClientModel = ClientManager.createClient(session, realm, adminClient); + + new ClientManager(new RealmManager(session)).enableServiceAccount(adminClientModel); + UserModel serviceAccount = session.users().getServiceAccount(adminClientModel); + RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); + serviceAccount.grantRole(adminRole); + + // TODO: set temporary + // also set the expiration - could be relative to a creation timestamp, or computed + + ServicesLogger.LOGGER.createdTemporaryAdminService(clientId); + } + + public void createMasterRealmUser(String username, String password) { + createTemporaryMasterRealmAdminUser(username, password, true); } } diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index ffd7a72e959..d2b7f985dfe 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -154,6 +154,7 @@ public abstract class KeycloakApplication extends Application { if (createMasterRealm) { applianceBootstrap.createMasterRealm(); + createTemporaryAdmin(session); } } }); @@ -169,6 +170,8 @@ public abstract class KeycloakApplication extends Application { return exportImportManager[0]; } + protected abstract void createTemporaryAdmin(KeycloakSession session); + protected void loadConfig() { ServiceLoader loader = ServiceLoader.load(ConfigProviderFactory.class, KeycloakApplication.class.getClassLoader()); diff --git a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java index 96a84d20733..9060fc26d81 100755 --- a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java @@ -139,7 +139,7 @@ public class WelcomeResource { applianceBootstrap.createMasterRealmUser(username, password); shouldBootstrap.set(false); - ServicesLogger.LOGGER.createdInitialAdminUser(username); + ServicesLogger.LOGGER.createdTemporaryAdminUser(username); return createWelcomePage("User created", null); } } diff --git a/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java b/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java index 5eab0110b78..5ef759c9cb8 100644 --- a/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java +++ b/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java @@ -18,6 +18,7 @@ package org.keycloak.services.resteasy; import org.keycloak.common.Profile; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.services.error.KcUnrecognizedPropertyExceptionHandler; import org.keycloak.services.error.KeycloakErrorHandler; @@ -85,4 +86,9 @@ public class ResteasyKeycloakApplication extends KeycloakApplication { return factory; } + @Override + protected void createTemporaryAdmin(KeycloakSession session) { + // do nothing + } + }