startup, welcome, and cli handling of bootstrap-admin user (#30054)
* fix: adding password and service account based bootstrap and recovery closes: #29324, #30002, #30003 Signed-off-by: Steve Hawkins <shawkins@redhat.com> * Fix tests Signed-off-by: Václav Muzikář <vmuzikar@redhat.com> --------- Signed-off-by: Steve Hawkins <shawkins@redhat.com> Signed-off-by: Václav Muzikář <vmuzikar@redhat.com> Co-authored-by: Václav Muzikář <vmuzikar@redhat.com>
This commit is contained in:
parent
02d64d959c
commit
96511e55c6
51 changed files with 879 additions and 223 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -98,6 +98,9 @@ node
|
|||
# Vite
|
||||
dist
|
||||
|
||||
!/quarkus/dist
|
||||
!/quarkus/**/src/**/dist
|
||||
|
||||
# ESLint
|
||||
.eslintcache
|
||||
|
||||
|
|
52
common/src/main/java/org/keycloak/common/util/IoUtils.java
Normal file
52
common/src/main/java/org/keycloak/common/util/IoUtils.java
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||
|
@ -61,7 +61,7 @@ public class SetPasswordCmd extends AbstractAuthOptionsCmd {
|
|||
}
|
||||
|
||||
if (pass == null) {
|
||||
pass = readSecret("Enter password: ");
|
||||
pass = readPasswordFromConsole("password");
|
||||
}
|
||||
|
||||
ConfigData config = loadConfig();
|
||||
|
|
|
@ -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;
|
||||
|
@ -177,7 +177,7 @@ public abstract class BaseAuthOptionsCmd extends BaseGlobalOptionsCmd {
|
|||
pass = System.getenv("KC_CLI_TRUSTSTORE_PASSWORD");
|
||||
}
|
||||
if (pass == null) {
|
||||
pass = IoUtil.readSecret("Enter truststore password: ");
|
||||
pass = IoUtils.readPasswordFromConsole("truststore password");
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||
|
@ -78,7 +78,7 @@ public class BaseConfigTruststoreCmd extends BaseAuthOptionsCmd {
|
|||
}
|
||||
|
||||
if ("-".equals(trustPass)) {
|
||||
trustPass = readSecret("Enter truststore password: ");
|
||||
trustPass = readPasswordFromConsole("truststore password");
|
||||
}
|
||||
|
||||
pass = trustPass;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||
|
@ -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();
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package org.keycloak.config;
|
||||
|
||||
public class BootstrapAdminOptions {
|
||||
|
||||
public static final Option<String> PASSWORD = new OptionBuilder<>("bootstrap-admin-password", String.class)
|
||||
.category(OptionCategory.BOOTSTRAP_ADMIN)
|
||||
.description("Bootstrap admin password")
|
||||
.hidden()
|
||||
.build();
|
||||
|
||||
public static final Option<String> USERNAME = new OptionBuilder<>("bootstrap-admin-username", String.class)
|
||||
.category(OptionCategory.BOOTSTRAP_ADMIN)
|
||||
.description("Username of the bootstrap admin")
|
||||
.hidden()
|
||||
.build();
|
||||
|
||||
public static final Option<Integer> 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<String> 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<String> CLIENT_SECRET = new OptionBuilder<>("bootstrap-admin-client-secret", String.class)
|
||||
.category(OptionCategory.BOOTSTRAP_ADMIN)
|
||||
.description("Client secret for the admin service")
|
||||
.hidden()
|
||||
.build();
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> 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> 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<String> cliArgs, List<CommandLine> 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<String> cliArgs, CommandLine cmd) {
|
||||
private static int runReAugmentationIfNeeded(List<String> 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<String> configArgsList = new ArrayList<>();
|
||||
configArgsList.add(Build.NAME);
|
||||
parseConfigArgs(cliArgs, (k, v) -> {
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(k);
|
||||
|
||||
List<String> 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<String> cliArgs) {
|
||||
public static CommandLine createCommandLine(Consumer<CommandSpec> 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<String> 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<String> 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<OptionCategory, List<PropertyMapper<?>>> 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);
|
||||
}
|
||||
|
|
|
@ -73,4 +73,5 @@ public abstract class AbstractCommand {
|
|||
public CommandLine getCommandLine() {
|
||||
return spec.commandLine();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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<OptionCategory> getOptionCategories() {
|
||||
EnumSet<OptionCategory> excludedCategories = excludedCategories();
|
||||
return super.getOptionCategories().stream().filter(optionCategory -> !excludedCategories.contains(optionCategory)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
protected EnumSet<OptionCategory> excludedCategories() {
|
||||
return EnumSet.of(OptionCategory.IMPORT, OptionCategory.EXPORT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<OptionCategory> 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<OptionCategory> excludedCategories() {
|
||||
return EnumSet.of(OptionCategory.IMPORT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<OptionCategory> 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<OptionCategory> excludedCategories() {
|
||||
return EnumSet.of(OptionCategory.EXPORT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<OptionCategory> getOptionCategories() {
|
||||
return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeRuntime() {
|
||||
return true;
|
||||
|
|
|
@ -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<OptionCategory> getOptionCategories() {
|
||||
return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeRuntime() {
|
||||
return true;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -153,7 +153,7 @@ public final class HttpPropertyMappers {
|
|||
boolean enabled = Boolean.parseBoolean(value.get());
|
||||
Optional<String> proxy = Configuration.getOptionalKcValue("proxy");
|
||||
|
||||
if (Environment.isDevMode() || Environment.isImportExportMode()
|
||||
if (Environment.isDevMode() || Environment.isNonServerMode()
|
||||
|| ("edge".equalsIgnoreCase(proxy.orElse("")))) {
|
||||
enabled = true;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"));
|
||||
|
|
98
quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BootstrapAdminDistTest.java
vendored
Normal file
98
quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BootstrapAdminDistTest.java
vendored
Normal file
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<String> arguments);
|
||||
default CLIResult run(String... arguments) {
|
||||
|
|
|
@ -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;
|
||||
|
@ -111,6 +112,50 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
|
|||
this.distPath = prepareDistribution();
|
||||
}
|
||||
|
||||
public CLIResult kcadm(String... arguments) throws IOException {
|
||||
return kcadm(Arrays.asList(arguments));
|
||||
}
|
||||
|
||||
public CLIResult kcadm(List<String> arguments) throws IOException {
|
||||
List<String> 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<String> outputStream = new ArrayList<>();
|
||||
List<String> errorStream = new ArrayList<>();
|
||||
readOutput(kcadm, outputStream, errorStream);
|
||||
|
||||
int exitValue = kcadm.exitValue();
|
||||
|
||||
return CLIResult.create(outputStream, errorStream, exitValue);
|
||||
}
|
||||
|
||||
private void invoke(List<String> allArgs, String cmd) {
|
||||
if (isWindows()) {
|
||||
allArgs.add(distPath.resolve("bin") + File.separator + cmd);
|
||||
} else {
|
||||
allArgs.add("./" + cmd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CLIResult run(List<String> arguments) {
|
||||
stop();
|
||||
|
@ -237,11 +282,7 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
|
|||
public String[] getCliArgs(List<String> arguments) {
|
||||
List<String> 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<String> outputStream, List<String> 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<String> stream) throws IOException {
|
||||
private static void readStream(BufferedReader reader, List<String> stream) throws IOException {
|
||||
String line;
|
||||
|
||||
while (reader.ready() && (line = reader.readLine()) != null) {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<ConfigProviderFactory> loader = ServiceLoader.load(ConfigProviderFactory.class, KeycloakApplication.class.getClassLoader());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue