fix: allows for the detection of a master realm with --import-realms (#32914)

also moving initial bootstrapping after import

closes: #32689

Signed-off-by: Steven Hawkins <shawkins@redhat.com>
Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Steven Hawkins 2024-09-30 08:40:16 -04:00 committed by GitHub
parent 53102521d2
commit 5d99d91818
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 112 additions and 58 deletions

View file

@ -300,6 +300,10 @@ The new event types are supported by the Email Event Listener.
The following event types are now deprecated and will be removed in a future version: `UPDATE_PASSWORD`, `UPDATE_PASSWORD_ERROR`, `UPDATE_TOTP`, `UPDATE_TOTP_ERROR`, `REMOVE_TOTP`, `REMOVE_TOTP_ERROR` The following event types are now deprecated and will be removed in a future version: `UPDATE_PASSWORD`, `UPDATE_PASSWORD_ERROR`, `UPDATE_TOTP`, `UPDATE_TOTP_ERROR`, `REMOVE_TOTP`, `REMOVE_TOTP_ERROR`
= `--import-realm` option can import the master realm
When running a `start` or `start-dev` command with the `--import-realm` option before the master realm exists, it will be imported if it exists in the import material. The previous behavior was that the master realm was created first, then its import skipped.
= BouncyCastle FIPS updated = BouncyCastle FIPS updated
Our FIPS 140-2 integration is now tested and supported with version 2 of BouncyCastle FIPS libraries. This version is certified with Java 21. If you use FIPS 140-2 integration, it is recommended to Our FIPS 140-2 integration is now tested and supported with version 2 of BouncyCastle FIPS libraries. This version is certified with Java 21. If you use FIPS 140-2 integration, it is recommended to

View file

@ -37,6 +37,11 @@ public abstract class ExportImportSessionTask implements KeycloakSessionTask {
throw new RuntimeException("Error during export/import: " + ioe.getMessage(), ioe); throw new RuntimeException("Error during export/import: " + ioe.getMessage(), ioe);
} }
} }
@Override
public boolean useExistingSession() {
return true;
}
protected abstract void runExportImportTask(KeycloakSession session) throws IOException; protected abstract void runExportImportTask(KeycloakSession session) throws IOException;
} }

View file

@ -20,7 +20,6 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL; import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL;
import java.io.File; import java.io.File;
import java.util.Optional;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import picocli.CommandLine; import picocli.CommandLine;

View file

@ -83,7 +83,8 @@ public class ImportAtStartupDistTest {
CLIResult result = dist.run("start-dev", "--import-realm"); CLIResult result = dist.run("start-dev", "--import-realm");
result.assertMessage("Realm 'quickstart-realm' imported"); result.assertMessage("Realm 'quickstart-realm' imported");
result.assertMessage("Realm 'master' already exists. Import skipped"); result.assertMessage("Realm 'master' imported");
result.assertNoMessage("Realm 'master' already exists. Import skipped");
} }
@Test @Test

View file

@ -262,7 +262,7 @@ public final class KeycloakModelUtils {
runJobInTransactionWithResult(factory, null, session -> { runJobInTransactionWithResult(factory, null, session -> {
task.run(session); task.run(session);
return null; return null;
}); }, task.useExistingSession());
} }
/** /**
@ -275,7 +275,7 @@ public final class KeycloakModelUtils {
runJobInTransactionWithResult(factory, context, session -> { runJobInTransactionWithResult(factory, context, session -> {
task.run(session); task.run(session);
return null; return null;
}); }, task.useExistingSession());
} }
/** /**
@ -365,7 +365,7 @@ public final class KeycloakModelUtils {
* @return The return value from the callable * @return The return value from the callable
*/ */
public static <V> V runJobInTransactionWithResult(KeycloakSessionFactory factory, final KeycloakSessionTaskWithResult<V> callable) { public static <V> V runJobInTransactionWithResult(KeycloakSessionFactory factory, final KeycloakSessionTaskWithResult<V> callable) {
return runJobInTransactionWithResult(factory, null, callable); return runJobInTransactionWithResult(factory, null, callable, false);
} }
/** /**
@ -374,13 +374,19 @@ public final class KeycloakModelUtils {
* @param factory The session factory * @param factory The session factory
* @param context The context from the previous session to use * @param context The context from the previous session to use
* @param callable The callable to execute * @param callable The callable to execute
* @param useExistingSession if the existing session should be used
* @return The return value from the callable * @return The return value from the callable
*/ */
public static <V> V runJobInTransactionWithResult(KeycloakSessionFactory factory, KeycloakContext context, final KeycloakSessionTaskWithResult<V> callable) { public static <V> V runJobInTransactionWithResult(KeycloakSessionFactory factory, KeycloakContext context, final KeycloakSessionTaskWithResult<V> callable, boolean useExistingSession) {
V result; V result;
KeycloakSession existing = KeycloakSessionUtil.getKeycloakSession();
if (useExistingSession && existing != null && existing.getTransactionManager().isActive()) {
return callable.run(existing);
}
try (KeycloakSession session = factory.create()) { try (KeycloakSession session = factory.create()) {
session.getTransactionManager().begin(); session.getTransactionManager().begin();
KeycloakSession old = KeycloakSessionUtil.setKeycloakSession(session); KeycloakSessionUtil.setKeycloakSession(session);
try { try {
cloneContextRealmClientToSession(context, session); cloneContextRealmClientToSession(context, session);
result = callable.run(session); result = callable.run(session);
@ -388,7 +394,7 @@ public final class KeycloakModelUtils {
session.getTransactionManager().setRollbackOnly(); session.getTransactionManager().setRollbackOnly();
throw t; throw t;
} finally { } finally {
KeycloakSessionUtil.setKeycloakSession(old); KeycloakSessionUtil.setKeycloakSession(existing);
} }
} }
return result; return result;

View file

@ -25,5 +25,9 @@ package org.keycloak.models;
public interface KeycloakSessionTask { public interface KeycloakSessionTask {
void run(KeycloakSession session); void run(KeycloakSession session);
default boolean useExistingSession() {
return false;
}
} }

View file

@ -21,8 +21,6 @@ import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import java.io.File; import java.io.File;
@ -33,7 +31,7 @@ import java.nio.file.Paths;
import java.util.HashSet; import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER; import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
@ -92,6 +90,20 @@ public class ExportImportManager {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public boolean isImportMasterIncludedAtStartup(String dir) {
if (dir == null) {
throw new IllegalStateException("Import not enabled");
}
return getStartupImportProviders(dir).map(Supplier::get).anyMatch(provider -> {
try {
return provider.isMasterRealmExported();
} catch (IOException e) {
throw new RuntimeException("Failed to run import", e);
}
});
}
public boolean isRunExport() { public boolean isRunExport() {
return exportProvider != null; return exportProvider != null;
@ -104,21 +116,38 @@ public class ExportImportManager {
throw new RuntimeException("Failed to run import", e); throw new RuntimeException("Failed to run import", e);
} }
} }
public void runImportAtStartup(String dir) throws IOException { public void runImportAtStartup(String dir) throws IOException {
System.setProperty(ExportImportConfig.STRATEGY, Strategy.IGNORE_EXISTING.toString());
ExportImportConfig.setReplacePlaceholders(true); ExportImportConfig.setReplacePlaceholders(true);
ExportImportConfig.setAction("import"); // enables logging of what is imported
ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT);
// TODO: ideally the static setting above should be unset after this is run
getStartupImportProviders(dir).map(Supplier::get).forEach(ip -> {
try {
ip.importModel();
} catch (IOException e) {
throw new RuntimeException("Failed to run import", e);
}
});
}
private Stream<Supplier<ImportProvider>> getStartupImportProviders(String dir) {
Stream<ProviderFactory> factories = sessionFactory.getProviderFactoriesStream(ImportProvider.class); Stream<ProviderFactory> factories = sessionFactory.getProviderFactoriesStream(ImportProvider.class);
for (ProviderFactory factory : factories.collect(Collectors.toList())) { return factories.flatMap(factory -> {
String providerId = factory.getId(); String providerId = factory.getId();
if ("dir".equals(providerId)) { if ("dir".equals(providerId)) {
ExportImportConfig.setDir(dir); Supplier<ImportProvider> func = () -> {
ImportProvider importProvider = session.getProvider(ImportProvider.class, providerId); ExportImportConfig.setDir(dir);
importProvider.importModel(); return session.getProvider(ImportProvider.class, providerId);
} else if ("singleFile".equals(providerId)) { };
return Stream.of(func);
}
if ("singleFile".equals(providerId)) {
Set<String> filesToImport = new HashSet<>(); Set<String> filesToImport = new HashSet<>();
File[] files = Paths.get(dir).toFile().listFiles(); File[] files = Paths.get(dir).toFile().listFiles();
@ -139,23 +168,14 @@ public class ExportImportManager {
filesToImport.add(file.getAbsolutePath()); filesToImport.add(file.getAbsolutePath());
} }
for (String file : filesToImport) { return filesToImport.stream().map(file -> () -> {
ExportImportConfig.setFile(file); ExportImportConfig.setFile(file);
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { return session.getProvider(ImportProvider.class, providerId);
@Override });
public void run(KeycloakSession session) {
ImportProvider importProvider = session.getProvider(ImportProvider.class, providerId);
try {
importProvider.importModel();
} catch (IOException cause) {
throw new RuntimeException(cause);
}
}
});
}
} }
} return Stream.empty();
});
} }
public void runExport() { public void runExport() {

View file

@ -20,9 +20,7 @@ import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.config.ConfigProviderFactory; import org.keycloak.config.ConfigProviderFactory;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.ExportImportManager; import org.keycloak.exportimport.ExportImportManager;
import org.keycloak.exportimport.Strategy;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.KeycloakSessionTask;
@ -50,6 +48,7 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
@ -118,10 +117,15 @@ public abstract class KeycloakApplication extends Application {
if (sessionFactory != null) if (sessionFactory != null)
sessionFactory.close(); sessionFactory.close();
} }
private static class BootstrapState {
ExportImportManager exportImportManager;
boolean newInstall;
}
// Bootstrap master realm, import realms and create admin user. // Bootstrap master realm, import realms and create admin user.
protected ExportImportManager bootstrap() { protected ExportImportManager bootstrap() {
ExportImportManager[] exportImportManager = new ExportImportManager[1]; BootstrapState bootstrapState = new BootstrapState();
logger.debug("bootstrap"); logger.debug("bootstrap");
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@ -145,29 +149,38 @@ public abstract class KeycloakApplication extends Application {
// TODO up here ^^ // TODO up here ^^
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session); ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
exportImportManager[0] = new ExportImportManager(session); var exportImportManager = bootstrapState.exportImportManager = new ExportImportManager(session);
bootstrapState.newInstall = applianceBootstrap.isNewInstall();
boolean createMasterRealm = applianceBootstrap.isNewInstall(); if (bootstrapState.newInstall) {
if (exportImportManager[0].isRunImport() && exportImportManager[0].isImportMasterIncluded()) { // check if this is an import command that is importing the master realm
createMasterRealm = false; boolean importingMaster = exportImportManager.isRunImport() && exportImportManager.isImportMasterIncluded();
} // check if this is a start command that is importing the master realm
importingMaster |= getImportDirectory().filter(exportImportManager::isImportMasterIncludedAtStartup).isPresent();
if (createMasterRealm) { if (!importingMaster) {
applianceBootstrap.createMasterRealm(); applianceBootstrap.createMasterRealm();
}
// these are also running in the initial bootstrap transaction - if there is a problem, the server won't be initialized at all
runImports(exportImportManager);
createTemporaryAdmin(session); createTemporaryAdmin(session);
} }
} }
}); });
if (exportImportManager[0].isRunImport()) { if (!bootstrapState.newInstall) {
exportImportManager[0].runImport(); runImports(bootstrapState.exportImportManager);
} else {
importRealms(exportImportManager[0]);
} }
importAddUser(); importAddUser();
return exportImportManager[0]; return bootstrapState.exportImportManager;
}
private void runImports(ExportImportManager exportImportManager) {
if (exportImportManager.isRunImport()) {
exportImportManager.runImport();
} else {
importRealms(exportImportManager);
}
} }
protected abstract void createTemporaryAdmin(KeycloakSession session); protected abstract void createTemporaryAdmin(KeycloakSession session);
@ -193,15 +206,17 @@ public abstract class KeycloakApplication extends Application {
} }
public void importRealms(ExportImportManager exportImportManager) { public void importRealms(ExportImportManager exportImportManager) {
String dir = System.getProperty("keycloak.import"); getImportDirectory().ifPresent(dir -> {
if (dir != null) {
try { try {
System.setProperty(ExportImportConfig.STRATEGY, Strategy.IGNORE_EXISTING.toString());
exportImportManager.runImportAtStartup(dir); exportImportManager.runImportAtStartup(dir);
} catch (IOException cause) { } catch (IOException cause) {
throw new RuntimeException("Failed to import realms", cause); throw new RuntimeException("Failed to import realms", cause);
} }
} });
}
private Optional<String> getImportDirectory() {
return Optional.ofNullable(System.getProperty("keycloak.import"));
} }
public void importRealm(RealmRepresentation rep, String from) { public void importRealm(RealmRepresentation rep, String from) {

View file

@ -1117,7 +1117,7 @@ public class RealmAdminResource {
AdminEventBuilder adminEventClone = adminEvent.clone(kcSession); AdminEventBuilder adminEventClone = adminEvent.clone(kcSession);
// calling a static method to avoid using the wrong instances // calling a static method to avoid using the wrong instances
return getPartialImportResults(requestBody, kcSession, realmClone, adminEventClone); return getPartialImportResults(requestBody, kcSession, realmClone, adminEventClone);
}) }, false)
).build(); ).build();
} catch (ModelDuplicateException e) { } catch (ModelDuplicateException e) {
throw ErrorResponse.exists(e.getLocalizedMessage()); throw ErrorResponse.exists(e.getLocalizedMessage());

View file

@ -299,7 +299,7 @@ public class ClientModelTest extends AbstractKeycloakTest {
client = realm.addClient(id, "application2"); client = realm.addClient(id, "application2");
return client.getId(); return client.getId();
}); }, false);
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), (KeycloakSession sessionAppWithId2) -> { KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), (KeycloakSession sessionAppWithId2) -> {
currentSession = sessionAppWithId2; currentSession = sessionAppWithId2;