Rework the export to use CLI options and property mappers

Also, adding the wiring to support Model tests for the export.

Closes #13613
This commit is contained in:
Alexander Schwartz 2023-02-27 14:14:11 +01:00 committed by Hynek Mlnařík
parent bb4ae872bd
commit f6f179eaca
18 changed files with 537 additions and 140 deletions

View file

@ -20,6 +20,7 @@ package org.keycloak.exportimport.dir;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.exportimport.util.MultipleStepsExportProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.platform.Platform;
@ -34,23 +35,28 @@ import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DirExportProvider extends MultipleStepsExportProvider {
public class DirExportProvider extends MultipleStepsExportProvider<DirExportProvider> {
private final File rootDirectory;
private String dir;
public DirExportProvider() {
private File rootDirectory;
public DirExportProvider(KeycloakSessionFactory sessionFactory) {
// Determine platform tmp directory
this.rootDirectory = new File(Platform.getPlatform().getTmpDirectory(), "keycloak-export");
this.rootDirectory.mkdirs();
logger.infof("Exporting into directory %s", this.rootDirectory.getAbsolutePath());
super(sessionFactory);
}
public DirExportProvider(File rootDirectory) {
this.rootDirectory = rootDirectory;
this.rootDirectory.mkdirs();
logger.infof("Exporting into directory %s", this.rootDirectory.getAbsolutePath());
private File getRootDirectory() {
if (rootDirectory == null) {
if (dir == null) {
rootDirectory = new File(Platform.getPlatform().getTmpDirectory(), "keycloak-export");
} else {
rootDirectory = new File(dir);
}
rootDirectory.mkdirs();
logger.infof("Exporting into directory %s", rootDirectory.getAbsolutePath());
}
return rootDirectory;
}
public static boolean recursiveDeleteDir(File dirPath) {
@ -72,7 +78,7 @@ public class DirExportProvider extends MultipleStepsExportProvider {
@Override
public void writeRealm(String fileName, RealmRepresentation rep) throws IOException {
File file = new File(this.rootDirectory, fileName);
File file = new File(getRootDirectory(), fileName);
try (FileOutputStream is = new FileOutputStream(file)) {
JsonSerialization.prettyMapper.writeValue(is, rep);
}
@ -80,14 +86,14 @@ public class DirExportProvider extends MultipleStepsExportProvider {
@Override
protected void writeUsers(String fileName, KeycloakSession session, RealmModel realm, List<UserModel> users) throws IOException {
File file = new File(this.rootDirectory, fileName);
File file = new File(getRootDirectory(), fileName);
FileOutputStream os = new FileOutputStream(file);
ExportUtils.exportUsersToStream(session, realm, users, JsonSerialization.prettyMapper, os);
}
@Override
protected void writeFederatedUsers(String fileName, KeycloakSession session, RealmModel realm, List<String> users) throws IOException {
File file = new File(this.rootDirectory, fileName);
File file = new File(getRootDirectory(), fileName);
FileOutputStream os = new FileOutputStream(file);
ExportUtils.exportFederatedUsersToStream(session, realm, users, JsonSerialization.prettyMapper, os);
}
@ -95,4 +101,10 @@ public class DirExportProvider extends MultipleStepsExportProvider {
@Override
public void close() {
}
public DirExportProvider withDir(String dir) {
this.dir = dir;
return this;
}
}

View file

@ -21,31 +21,52 @@ import org.keycloak.Config;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.ExportProvider;
import org.keycloak.exportimport.ExportProviderFactory;
import org.keycloak.exportimport.UsersExportStrategy;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.io.File;
import java.util.List;
import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_USERS_EXPORT_STRATEGY;
import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_USERS_PER_FILE;
/**
* Construct a {@link DirExportProviderFactory} to be used to export one or more realms.
* For the sake of testing in the legacy testing setup, configurations can be overwritten via system properties.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DirExportProviderFactory implements ExportProviderFactory {
public static final String PROVIDER_ID = "dir";
public static final String DIR = "dir";
public static final String REALM_NAME = "realmName";
public static final String USERS_EXPORT_STRATEGY = "usersExportStrategy";
public static final String USERS_PER_FILE = "usersPerFile";
private Config.Scope config;
@Override
public ExportProvider create(KeycloakSession session) {
String dir = ExportImportConfig.getDir();
return dir!=null ? new DirExportProvider(new File(dir)) : new DirExportProvider();
String dir = System.getProperty(ExportImportConfig.DIR, config.get(DIR));
String realmName = System.getProperty(ExportImportConfig.REALM_NAME, config.get(REALM_NAME));
String usersExportStrategy = System.getProperty(ExportImportConfig.USERS_EXPORT_STRATEGY, config.get(USERS_EXPORT_STRATEGY, DEFAULT_USERS_EXPORT_STRATEGY.toString()));
String usersPerFile = System.getProperty(ExportImportConfig.USERS_PER_FILE, config.get(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE)));
return new DirExportProvider(session.getKeycloakSessionFactory())
.withDir(dir)
.withRealmName(realmName)
.withUsersExportStrategy(Enum.valueOf(UsersExportStrategy.class, usersExportStrategy.toUpperCase()))
.withUsersPerFile(Integer.parseInt(usersPerFile.trim()));
}
@Override
public void init(Config.Scope config) {
this.config = config;
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
@ -56,4 +77,37 @@ public class DirExportProviderFactory implements ExportProviderFactory {
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigMetadata() {
return ProviderConfigurationBuilder.create()
.property()
.name(REALM_NAME)
.type("string")
.helpText("Realm to export")
.add()
.property()
.name(DIR)
.type("string")
.helpText("Directory to export to")
.add()
.property()
.name(USERS_EXPORT_STRATEGY)
.type("string")
.helpText("Users export strategy")
.defaultValue(DEFAULT_USERS_EXPORT_STRATEGY)
.add()
.property()
.name(USERS_PER_FILE)
.type("int")
.helpText("Users per exported file")
.defaultValue(DEFAULT_USERS_PER_FILE)
.add()
.build();
}
}

View file

@ -28,11 +28,13 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.util.ObjectMapperResolver;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Objects;
import java.util.stream.Stream;
/**
@ -44,38 +46,48 @@ public class SingleFileExportProvider implements ExportProvider {
private File file;
public SingleFileExportProvider(File file) {
this.file = file;
private final KeycloakSessionFactory factory;
private String realmName;
public SingleFileExportProvider(KeycloakSessionFactory factory) {
this.factory = factory;
}
public void setFile(File file) {
public SingleFileExportProvider withFile(File file) {
this.file = file;
return this;
}
@Override
public void exportModel(KeycloakSessionFactory factory) throws IOException {
logger.infof("Exporting model into file %s", this.file.getAbsolutePath());
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
public void exportModel() {
if (realmName != null) {
ServicesLogger.LOGGER.realmExportRequested(realmName);
exportRealm(realmName);
} else {
ServicesLogger.LOGGER.fullModelExportRequested();
logger.infof("Exporting model into file %s", this.file.getAbsolutePath());
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
Stream<RealmRepresentation> realms = session.realms().getRealmsStream()
.map(realm -> ExportUtils.exportRealm(session, realm, true, true));
writeToFile(realms);
}
});
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
Stream<RealmRepresentation> realms = session.realms().getRealmsStream()
.map(realm -> ExportUtils.exportRealm(session, realm, true, true));
writeToFile(realms);
}
});
}
ServicesLogger.LOGGER.exportSuccess();
}
@Override
public void exportRealm(KeycloakSessionFactory factory, final String realmName) throws IOException {
private void exportRealm(final String realmName) {
logger.infof("Exporting realm '%s' into file %s", realmName, this.file.getAbsolutePath());
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
RealmModel realm = session.realms().getRealmByName(realmName);
Objects.requireNonNull(realm, "realm not found by realm name '" + realmName + "'");
RealmRepresentation realmRep = ExportUtils.exportRealm(session, realm, true, true);
writeToFile(realmRep);
}
@ -98,4 +110,9 @@ public class SingleFileExportProvider implements ExportProvider {
FileOutputStream stream = new FileOutputStream(this.file);
getObjectMapper().writeValue(stream, reps);
}
public ExportProvider withRealmName(String realmName) {
this.realmName = realmName;
return this;
}
}

View file

@ -23,29 +23,41 @@ import org.keycloak.exportimport.ExportProvider;
import org.keycloak.exportimport.ExportProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.io.File;
import java.util.List;
import java.util.Objects;
/**
* Construct a {@link SingleFileExportProvider} to be used to export one or more realms.
* For the sake of testing in the legacy testing setup, configurations can be overwritten via system properties.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SingleFileExportProviderFactory implements ExportProviderFactory {
public static final String PROVIDER_ID = "singleFile";
public static final String FILE = "file";
public static final String REALM_NAME = "realmName";
private Config.Scope config;
@Override
public ExportProvider create(KeycloakSession session) {
String fileName = ExportImportConfig.getFile();
return new SingleFileExportProvider(new File(fileName));
String fileName = System.getProperty(ExportImportConfig.FILE, config.get(FILE));
Objects.requireNonNull(fileName, "file name not configured");
String realmName = System.getProperty(ExportImportConfig.REALM_NAME, config.get(REALM_NAME));
return new SingleFileExportProvider(session.getKeycloakSessionFactory()).withFile(new File(fileName)).withRealmName(realmName);
}
@Override
public void init(Config.Scope config) {
this.config = config;
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
@ -56,4 +68,23 @@ public class SingleFileExportProviderFactory implements ExportProviderFactory {
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigMetadata() {
return ProviderConfigurationBuilder.create()
.property()
.name(REALM_NAME)
.type("string")
.helpText("Realm to export")
.add()
.property()
.name(FILE)
.type("string")
.helpText("File to export to")
.add()
.build();
}
}

View file

@ -18,16 +18,15 @@
package org.keycloak.exportimport.util;
import org.jboss.logging.Logger;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.ExportProvider;
import org.keycloak.exportimport.UsersExportStrategy;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.ServicesLogger;
import org.keycloak.storage.UserStorageUtil;
import java.io.IOException;
@ -38,37 +37,55 @@ import java.util.stream.Collectors;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class MultipleStepsExportProvider implements ExportProvider {
public abstract class MultipleStepsExportProvider<T extends MultipleStepsExportProvider<?>> implements ExportProvider {
protected final Logger logger = Logger.getLogger(getClass());
protected final KeycloakSessionFactory factory;
private String realmId;
private int usersPerFile;
private UsersExportStrategy usersExportStrategy;
public MultipleStepsExportProvider(KeycloakSessionFactory factory) {
this.factory = factory;
}
@Override
public void exportModel(KeycloakSessionFactory factory) throws IOException {
final RealmsHolder holder = new RealmsHolder();
KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
List<RealmModel> realms = session.realms().getRealmsStream().collect(Collectors.toList());
holder.realms = realms;
public void exportModel() {
if (realmId != null) {
ServicesLogger.LOGGER.realmExportRequested(realmId);
exportRealm(realmId);
} else {
ServicesLogger.LOGGER.fullModelExportRequested();
List<RealmModel> realms = KeycloakModelUtils.runJobInTransactionWithResult(factory, session -> session.realms().getRealmsStream().collect(Collectors.toList()));
for (RealmModel realm : realms) {
exportRealmImpl(realm.getName());
}
});
for (RealmModel realm : holder.realms) {
exportRealmImpl(factory, realm.getName());
}
ServicesLogger.LOGGER.exportSuccess();
}
@Override
public void exportRealm(KeycloakSessionFactory factory, String realmName) throws IOException {
exportRealmImpl(factory, realmName);
public T withRealmName(String realmName) {
this.realmId = realmName;
return (T) this;
}
protected void exportRealmImpl(KeycloakSessionFactory factory, final String realmName) throws IOException {
final UsersExportStrategy usersExportStrategy = ExportImportConfig.getUsersExportStrategy();
final int usersPerFile = ExportImportConfig.getUsersPerFile();
public T withUsersPerFile(int usersPerFile) {
this.usersPerFile = usersPerFile;
return (T) this;
}
public T withUsersExportStrategy(UsersExportStrategy usersExportStrategy) {
this.usersExportStrategy = usersExportStrategy;
return (T) this;
}
public void exportRealm(String realmName) {
exportRealmImpl(realmName);
}
protected void exportRealmImpl(final String realmName) {
final UsersHolder usersHolder = new UsersHolder();
final boolean exportUsersIntoRealmFile = usersExportStrategy == UsersExportStrategy.REALM_FILE;
FederatedUsersHolder federatedUsersHolder = new FederatedUsersHolder();
@ -168,11 +185,6 @@ public abstract class MultipleStepsExportProvider implements ExportProvider {
protected abstract void writeUsers(String fileName, KeycloakSession session, RealmModel realm, List<UserModel> users) throws IOException;
protected abstract void writeFederatedUsers(String fileName, KeycloakSession session, RealmModel realm, List<String> users) throws IOException;
public static class RealmsHolder {
List<RealmModel> realms;
}
public static class UsersHolder {
List<UserModel> users;
int totalCount;

View file

@ -0,0 +1,56 @@
/*
* 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.config;
public class ExportOptions {
public static final Option<String> FILE = new OptionBuilder<>("file", String.class)
.category(OptionCategory.EXPORT)
.hidden() // hidden for now until we refactor the import command
.buildTime(false)
.build();
public static final Option<String> DIR = new OptionBuilder<>("dir", String.class)
.category(OptionCategory.EXPORT)
.hidden() // hidden for now until we refactor the import command
.buildTime(false)
.build();
public static final Option<String> REALM = new OptionBuilder<>("realm", String.class)
.category(OptionCategory.EXPORT)
.description("Set the name of the realm to export. If not set, all realms are going to be exported.")
.buildTime(false)
.build();
public static final Option<Integer> USERS_PER_FILE = new OptionBuilder<>("users-per-file", Integer.class)
.category(OptionCategory.EXPORT)
.defaultValue(50)
.description("Set the number of users per file. It is used only if 'users' is set to 'different_files'.")
.buildTime(false)
.build();
public static final Option<String> USERS = new OptionBuilder<>("users", String.class)
.category(OptionCategory.EXPORT)
.defaultValue("different_files")
.description("Set how users should be exported.")
// see UsersExportStrategy
.expectedValues("skip", "realm_file", "same_file", "different_files")
.buildTime(false)
.build();
}

View file

@ -15,6 +15,7 @@ public enum OptionCategory {
VAULT("Vault", 100, ConfigSupportLevel.SUPPORTED),
LOGGING("Logging", 110, ConfigSupportLevel.SUPPORTED),
SECURITY("Security", 120, ConfigSupportLevel.PREVIEW),
EXPORT("Export", 130, ConfigSupportLevel.SUPPORTED),
GENERAL("General", 999, ConfigSupportLevel.SUPPORTED);
private String heading;

View file

@ -366,7 +366,7 @@ public final class Picocli {
boolean includeBuildTime = false;
boolean includeRuntime = false;
if (Start.NAME.equals(command.name()) || StartDev.NAME.equals(command.name())) {
if (Start.NAME.equals(command.name()) || StartDev.NAME.equals(command.name()) || Export.NAME.equals(command.name())) {
includeBuildTime = isRebuilt() || !cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG);
includeRuntime = true;
} else if (Build.NAME.equals(command.name())) {
@ -413,6 +413,18 @@ public final class Picocli {
private static void addMappedOptionsToArgGroups(CommandSpec cSpec, Map<OptionCategory, List<PropertyMapper>> propertyMappers) {
for(OptionCategory category : OptionCategory.values()) {
if (cSpec.name().equals(Export.NAME)) {
// The export command should show only export options
if (category != OptionCategory.EXPORT) {
continue;
}
} else {
// No other command should have the export options
if (category == OptionCategory.EXPORT) {
continue;
}
}
List<PropertyMapper> mappersInCategory = propertyMappers.get(category);
if (mappersInCategory == null) {

View file

@ -19,9 +19,11 @@ package org.keycloak.quarkus.runtime.cli.command;
import org.keycloak.quarkus.runtime.Environment;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT;
public abstract class AbstractExportImportCommand extends AbstractStartCommand implements Runnable {
private final String action;
@ -46,14 +48,21 @@ public abstract class AbstractExportImportCommand extends AbstractStartCommand i
public void run() {
System.setProperty("keycloak.migration.action", action);
if (toDir != null) {
System.setProperty("keycloak.migration.provider", "dir");
System.setProperty("keycloak.migration.dir", toDir);
} else if (toFile != null) {
System.setProperty("keycloak.migration.provider", "singleFile");
System.setProperty("keycloak.migration.file", toFile);
} else {
executionError(spec.commandLine(), "Must specify either --dir or --file options.");
if (action.equals(ACTION_IMPORT)) {
if (toDir != null) {
System.setProperty("keycloak.migration.provider", "dir");
System.setProperty("keycloak.migration.dir", toDir);
} else if (toFile != null) {
System.setProperty("keycloak.migration.provider", "singleFile");
System.setProperty("keycloak.migration.file", toFile);
} else {
executionError(spec.commandLine(), "Must specify either --dir or --file options.");
}
} else if (action.equals(ACTION_EXPORT)) {
// for export, the properties are no longer needed and will be passed by the property mappers
if (toDir == null && toFile == null) {
executionError(spec.commandLine(), "Must specify either --dir or --file options.");
}
}
Environment.setProfile(Environment.IMPORT_EXPORT_MODE);

View file

@ -29,40 +29,8 @@ public final class Export extends AbstractExportImportCommand implements Runnabl
public static final String NAME = "export";
@Option(names = "--users",
arity = "1",
description = "Set how users should be exported. Possible values are: skip, realm_file, same_file, different_files.",
paramLabel = "<strategy>",
defaultValue = "different_files")
String users;
@Option(names = "--users-per-file",
arity = "1",
description = "Set the number of users per file. Its used only if --users=different_files.",
paramLabel = "<number>",
defaultValue = "50")
Integer usersPerFile;
@Option(names = "--realm",
arity = "1",
description = "Set the name of the realm to export. If not set, all realms are going to be exported.",
paramLabel = "<realm>")
String realm;
public Export() {
super(ACTION_EXPORT);
}
@Override
protected void doBeforeRun() {
if (realm != null) {
System.setProperty("keycloak.migration.realmName", realm);
}
System.setProperty("keycloak.migration.usersExportStrategy", users.toUpperCase());
if (usersPerFile != null) {
System.setProperty("keycloak.migration.usersPerFile", usersPerFile.toString());
}
}
}

View file

@ -0,0 +1,78 @@
/*
* 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 io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import org.keycloak.config.ExportOptions;
import java.util.Optional;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class ExportPropertyMappers {
private ExportPropertyMappers() {
}
public static PropertyMapper<?>[] getMappers() {
return new PropertyMapper[] {
fromOption(ExportOptions.FILE)
.to("kc.spi-export-exporter")
.transformer(ExportPropertyMappers::transformExporter)
.paramLabel("file")
.build(),
fromOption(ExportOptions.FILE)
.to("kc.spi-export-single-file-file")
.paramLabel("file")
.build(),
fromOption(ExportOptions.DIR)
.to("kc.spi-export-dir-dir")
.paramLabel("dir")
.build(),
fromOption(ExportOptions.REALM)
.to("kc.spi-export-single-file-realm-name")
.paramLabel("realm")
.build(),
fromOption(ExportOptions.REALM)
.to("kc.spi-export-dir-realm-name")
.paramLabel("realm")
.build(),
fromOption(ExportOptions.USERS)
.to("kc.spi-export-dir-users-export-strategy")
.paramLabel("strategy")
.build(),
fromOption(ExportOptions.USERS_PER_FILE)
.to("kc.spi-export-dir-users-per-file")
.paramLabel("number")
.build()
};
}
private static Optional<String> transformExporter(Optional<String> option, ConfigSourceInterceptorContext context) {
if (option.isPresent()) {
return Optional.of("singleFile");
}
ConfigValue dirConfigValue = context.proceed("kc.spi-export-dir-dir");
if (dirConfigValue != null && dirConfigValue.getValue() != null) {
return Optional.of("dir");
}
return Optional.empty();
}
}

View file

@ -38,6 +38,7 @@ public final class PropertyMappers {
MAPPERS.addAll(StoragePropertyMappers.getMappers());
MAPPERS.addAll(ClassLoaderPropertyMappers.getMappers());
MAPPERS.addAll(SecurityPropertyMappers.getMappers());
MAPPERS.addAll(ExportPropertyMappers.getMappers());
}
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {

View file

@ -27,8 +27,6 @@ import java.io.IOException;
*/
public interface ExportProvider extends Provider {
void exportModel(KeycloakSessionFactory factory) throws IOException;
void exportRealm(KeycloakSessionFactory factory, String realmName) throws IOException;
void exportModel() throws IOException;
}

View file

@ -98,24 +98,6 @@ public class ExportImportConfig {
System.setProperty(FILE, file);
}
public static UsersExportStrategy getUsersExportStrategy() {
String usersExportStrategy = System.getProperty(USERS_EXPORT_STRATEGY, DEFAULT_USERS_EXPORT_STRATEGY.toString());
return Enum.valueOf(UsersExportStrategy.class, usersExportStrategy);
}
public static void setUsersExportStrategy(UsersExportStrategy usersExportStrategy) {
System.setProperty(USERS_EXPORT_STRATEGY, usersExportStrategy.toString());
}
public static Integer getUsersPerFile() {
String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE));
return Integer.parseInt(usersPerFile.trim());
}
public static void setUsersPerFile(Integer usersPerFile) {
System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile));
}
public static Strategy getStrategy() {
String strategy = System.getProperty(STRATEGY, DEFAULT_STRATEGY.toString());
return Enum.valueOf(Strategy.class, strategy);

View file

@ -19,6 +19,7 @@ package org.keycloak.exportimport;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
@ -36,6 +37,9 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER_DEFAULT;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -61,6 +65,12 @@ public class ExportImportManager {
String exportImportAction = ExportImportConfig.getAction();
if (ExportImportConfig.ACTION_EXPORT.equals(exportImportAction)) {
// Future Refactoring: If the system properties are no longer needed for integration tests, refactor to use
// a default provider in its standard way.
// Setting this to "provider" doesn't work yet when instrumenting Keycloak with Quarkus as it leads to
// "java.lang.NullPointerException: Cannot invoke "String.indexOf(String)" because "value" is null"
// when calling "Config.getProvider()" from "KeycloakProcessor.loadFactories()"
providerId = Config.scope("export").get("exporter", System.getProperty(PROVIDER, PROVIDER_DEFAULT));
exportProvider = session.getProvider(ExportProvider.class, providerId);
if (exportProvider == null) {
throw new RuntimeException("Export provider '" + providerId + "' not found");
@ -161,16 +171,9 @@ public class ExportImportManager {
public void runExport() {
try {
if (realmName == null) {
ServicesLogger.LOGGER.fullModelExportRequested();
exportProvider.exportModel(sessionFactory);
} else {
ServicesLogger.LOGGER.realmExportRequested(realmName);
exportProvider.exportRealm(sessionFactory, realmName);
}
ServicesLogger.LOGGER.exportSuccess();
exportProvider.exportModel();
} catch (IOException e) {
throw new RuntimeException("Failed to run export");
throw new RuntimeException("Failed to run export", e);
}
}

View file

@ -0,0 +1,148 @@
/*
* Copyright 2022 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.testsuite.model.export;
import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.ExportImportManager;
import org.keycloak.exportimport.ExportProvider;
import org.keycloak.exportimport.dir.DirExportProviderFactory;
import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.testsuite.model.KeycloakModelTest;
import org.keycloak.testsuite.model.RequireProvider;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RequireProvider(value = ExportProvider.class)
public class ExportModelTest extends KeycloakModelTest {
public static final String REALM_NAME = "realm";
private String realmId;
@Override
public void createEnvironment(KeycloakSession s) {
// initialize a minimal realm with necessary entries to avoid any NPEs
RealmModel realm = createRealm(s, REALM_NAME);
realm.setSslRequired(SslRequired.NONE);
RoleModel role = s.roles().addRealmRole(realm, "default");
realm.setDefaultRole(role);
this.realmId = realm.getId();
}
@Override
public void cleanEnvironment(KeycloakSession s) {
s.realms().removeRealm(realmId);
}
@Test
@RequireProvider(value = ExportProvider.class, only = SingleFileExportProviderFactory.PROVIDER_ID)
public void testExportSingleFile() throws IOException {
try {
Path exportFolder = prepareTestFolder();
Path singleFileExport = exportFolder.resolve("singleFileExport.json");
CONFIG.spi("export")
.config("exporter", SingleFileExportProviderFactory.PROVIDER_ID);
CONFIG.spi("export")
.provider(SingleFileExportProviderFactory.PROVIDER_ID)
.config(SingleFileExportProviderFactory.FILE, singleFileExport.toAbsolutePath().toString());
CONFIG.spi("export")
.provider(SingleFileExportProviderFactory.PROVIDER_ID)
.config(SingleFileExportProviderFactory.REALM_NAME, REALM_NAME);
withRealm(realmId, (session, realm) -> {
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
ExportImportManager exportImportManager = new ExportImportManager(session);
exportImportManager.runExport();
return null;
});
// file will exist if export was successful
Assert.assertTrue(Files.exists(singleFileExport));
} finally {
CONFIG.spi("export")
.config("exporter", null);
CONFIG.spi("export")
.provider(SingleFileExportProviderFactory.PROVIDER_ID)
.config(SingleFileExportProviderFactory.FILE, null);
CONFIG.spi("export")
.provider(SingleFileExportProviderFactory.PROVIDER_ID)
.config(SingleFileExportProviderFactory.REALM_NAME, null);
}
}
@Test
@RequireProvider(value = ExportProvider.class, only = DirExportProviderFactory.PROVIDER_ID)
public void testExportDirectory() throws IOException {
try {
Path exportFolder = prepareTestFolder();
CONFIG.spi("export")
.config("exporter", DirExportProviderFactory.PROVIDER_ID);
CONFIG.spi("export")
.provider(DirExportProviderFactory.PROVIDER_ID)
.config(DirExportProviderFactory.DIR, exportFolder.toAbsolutePath().toString());
CONFIG.spi("export")
.provider(DirExportProviderFactory.PROVIDER_ID)
.config(DirExportProviderFactory.REALM_NAME, REALM_NAME);
withRealm(realmId, (session, realm) -> {
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
ExportImportManager exportImportManager = new ExportImportManager(session);
exportImportManager.runExport();
return null;
});
// file will exist if export was successful
Assert.assertTrue(Files.exists(exportFolder.resolve(REALM_NAME + "-realm.json")));
} finally {
CONFIG.spi("export")
.config("exporter", null);
CONFIG.spi("export")
.provider(DirExportProviderFactory.PROVIDER_ID)
.config(DirExportProviderFactory.DIR, null);
CONFIG.spi("export")
.provider(DirExportProviderFactory.PROVIDER_ID)
.config(DirExportProviderFactory.REALM_NAME, null);
}
}
@Rule
public TestName name = new TestName();
private Path prepareTestFolder() throws IOException {
Path singleFileExportFolder = Paths.get("target", "test", this.getClass().getName(), name.getMethodName());
if (singleFileExportFolder.toFile().exists()) {
FileUtils.deleteDirectory(singleFileExportFolder.toFile());
}
Assert.assertTrue(singleFileExportFolder.toFile().mkdirs());
return singleFileExportFolder;
}
}

View file

@ -16,7 +16,12 @@
*/
package org.keycloak.testsuite.model.parameters;
import org.keycloak.exportimport.ExportSpi;
import org.keycloak.exportimport.dir.DirExportProviderFactory;
import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.services.clientpolicy.ClientPolicyManagerFactory;
import org.keycloak.services.clientpolicy.ClientPolicyManagerSpi;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
import org.keycloak.provider.ProviderFactory;
@ -32,10 +37,15 @@ import java.util.Set;
public class ConcurrentHashMapStorage extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(ExportSpi.class)
.add(ClientPolicyManagerSpi.class)
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(ConcurrentHashMapStorageProviderFactory.class)
.add(SingleFileExportProviderFactory.class)
.add(DirExportProviderFactory.class)
.add(ClientPolicyManagerFactory.class)
.build();
@Override

View file

@ -18,6 +18,9 @@ package org.keycloak.testsuite.model.parameters;
import org.keycloak.authorization.store.StoreFactorySpi;
import org.keycloak.events.EventStoreSpi;
import org.keycloak.exportimport.ExportSpi;
import org.keycloak.exportimport.dir.DirExportProviderFactory;
import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
import org.keycloak.keys.PublicKeyStorageSpi;
import org.keycloak.models.DeploymentStateSpi;
import org.keycloak.models.SingleUseObjectProviderFactory;
@ -34,6 +37,8 @@ import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory;
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
import org.keycloak.services.clientpolicy.ClientPolicyManagerFactory;
import org.keycloak.services.clientpolicy.ClientPolicyManagerSpi;
import org.keycloak.sessions.AuthenticationSessionSpi;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.models.map.client.MapClientProviderFactory;