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:
Steven Hawkins 2024-07-03 09:23:40 -04:00 committed by GitHub
parent 02d64d959c
commit 96511e55c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 879 additions and 223 deletions

3
.gitignore vendored
View file

@ -98,6 +98,9 @@ node
# Vite
dist
!/quarkus/dist
!/quarkus/**/src/**/dist
# ESLint
.eslintcache

View 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);
}
}

View file

@ -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();

View file

@ -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 {

View file

@ -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");
}
}

View file

@ -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;

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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();
}

View file

@ -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;

View file

@ -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() {

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -73,4 +73,5 @@ public abstract class AbstractCommand {
public CommandLine getCommandLine() {
return spec.commandLine();
}
}

View file

@ -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) {
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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)

View file

@ -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;

View file

@ -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;

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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());
}

View file

@ -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;

View file

@ -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

View file

@ -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");

View file

@ -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"));

View 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"));
}
}

View file

@ -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();
}
}

View file

@ -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");
});
}

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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);
}

View file

@ -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();

View file

@ -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) {

View file

@ -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) {

View file

@ -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")

View file

@ -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);
}
}

View file

@ -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());

View file

@ -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);
}
}

View file

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