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
+ }
+
}