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:
parent
bb4ae872bd
commit
f6f179eaca
18 changed files with 537 additions and 140 deletions
|
@ -20,6 +20,7 @@ package org.keycloak.exportimport.dir;
|
||||||
import org.keycloak.exportimport.util.ExportUtils;
|
import org.keycloak.exportimport.util.ExportUtils;
|
||||||
import org.keycloak.exportimport.util.MultipleStepsExportProvider;
|
import org.keycloak.exportimport.util.MultipleStepsExportProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.platform.Platform;
|
import org.keycloak.platform.Platform;
|
||||||
|
@ -34,23 +35,28 @@ import java.util.List;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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
|
// Determine platform tmp directory
|
||||||
this.rootDirectory = new File(Platform.getPlatform().getTmpDirectory(), "keycloak-export");
|
super(sessionFactory);
|
||||||
this.rootDirectory.mkdirs();
|
|
||||||
|
|
||||||
logger.infof("Exporting into directory %s", this.rootDirectory.getAbsolutePath());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DirExportProvider(File rootDirectory) {
|
private File getRootDirectory() {
|
||||||
this.rootDirectory = rootDirectory;
|
if (rootDirectory == null) {
|
||||||
this.rootDirectory.mkdirs();
|
if (dir == null) {
|
||||||
|
rootDirectory = new File(Platform.getPlatform().getTmpDirectory(), "keycloak-export");
|
||||||
logger.infof("Exporting into directory %s", this.rootDirectory.getAbsolutePath());
|
} else {
|
||||||
|
rootDirectory = new File(dir);
|
||||||
|
}
|
||||||
|
rootDirectory.mkdirs();
|
||||||
|
logger.infof("Exporting into directory %s", rootDirectory.getAbsolutePath());
|
||||||
|
}
|
||||||
|
return rootDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean recursiveDeleteDir(File dirPath) {
|
public static boolean recursiveDeleteDir(File dirPath) {
|
||||||
|
@ -72,7 +78,7 @@ public class DirExportProvider extends MultipleStepsExportProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeRealm(String fileName, RealmRepresentation rep) throws IOException {
|
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)) {
|
try (FileOutputStream is = new FileOutputStream(file)) {
|
||||||
JsonSerialization.prettyMapper.writeValue(is, rep);
|
JsonSerialization.prettyMapper.writeValue(is, rep);
|
||||||
}
|
}
|
||||||
|
@ -80,14 +86,14 @@ public class DirExportProvider extends MultipleStepsExportProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void writeUsers(String fileName, KeycloakSession session, RealmModel realm, List<UserModel> users) throws IOException {
|
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);
|
FileOutputStream os = new FileOutputStream(file);
|
||||||
ExportUtils.exportUsersToStream(session, realm, users, JsonSerialization.prettyMapper, os);
|
ExportUtils.exportUsersToStream(session, realm, users, JsonSerialization.prettyMapper, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void writeFederatedUsers(String fileName, KeycloakSession session, RealmModel realm, List<String> users) throws IOException {
|
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);
|
FileOutputStream os = new FileOutputStream(file);
|
||||||
ExportUtils.exportFederatedUsersToStream(session, realm, users, JsonSerialization.prettyMapper, os);
|
ExportUtils.exportFederatedUsersToStream(session, realm, users, JsonSerialization.prettyMapper, os);
|
||||||
}
|
}
|
||||||
|
@ -95,4 +101,10 @@ public class DirExportProvider extends MultipleStepsExportProvider {
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DirExportProvider withDir(String dir) {
|
||||||
|
this.dir = dir;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,31 +21,52 @@ import org.keycloak.Config;
|
||||||
import org.keycloak.exportimport.ExportImportConfig;
|
import org.keycloak.exportimport.ExportImportConfig;
|
||||||
import org.keycloak.exportimport.ExportProvider;
|
import org.keycloak.exportimport.ExportProvider;
|
||||||
import org.keycloak.exportimport.ExportProviderFactory;
|
import org.keycloak.exportimport.ExportProviderFactory;
|
||||||
|
import org.keycloak.exportimport.UsersExportStrategy;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class DirExportProviderFactory implements ExportProviderFactory {
|
public class DirExportProviderFactory implements ExportProviderFactory {
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "dir";
|
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
|
@Override
|
||||||
public ExportProvider create(KeycloakSession session) {
|
public ExportProvider create(KeycloakSession session) {
|
||||||
String dir = ExportImportConfig.getDir();
|
String dir = System.getProperty(ExportImportConfig.DIR, config.get(DIR));
|
||||||
return dir!=null ? new DirExportProvider(new File(dir)) : new DirExportProvider();
|
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
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -56,4 +77,37 @@ public class DirExportProviderFactory implements ExportProviderFactory {
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,11 +28,13 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.util.ObjectMapperResolver;
|
import org.keycloak.services.util.ObjectMapperResolver;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,38 +46,48 @@ public class SingleFileExportProvider implements ExportProvider {
|
||||||
|
|
||||||
private File file;
|
private File file;
|
||||||
|
|
||||||
public SingleFileExportProvider(File file) {
|
private final KeycloakSessionFactory factory;
|
||||||
this.file = file;
|
private String realmName;
|
||||||
|
|
||||||
|
public SingleFileExportProvider(KeycloakSessionFactory factory) {
|
||||||
|
this.factory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFile(File file) {
|
public SingleFileExportProvider withFile(File file) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exportModel(KeycloakSessionFactory factory) throws IOException {
|
public void exportModel() {
|
||||||
logger.infof("Exporting model into file %s", this.file.getAbsolutePath());
|
if (realmName != null) {
|
||||||
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
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
|
@Override
|
||||||
protected void runExportImportTask(KeycloakSession session) throws IOException {
|
protected void runExportImportTask(KeycloakSession session) throws IOException {
|
||||||
Stream<RealmRepresentation> realms = session.realms().getRealmsStream()
|
Stream<RealmRepresentation> realms = session.realms().getRealmsStream()
|
||||||
.map(realm -> ExportUtils.exportRealm(session, realm, true, true));
|
.map(realm -> ExportUtils.exportRealm(session, realm, true, true));
|
||||||
|
|
||||||
writeToFile(realms);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
writeToFile(realms);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ServicesLogger.LOGGER.exportSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void exportRealm(final String realmName) {
|
||||||
public void exportRealm(KeycloakSessionFactory factory, final String realmName) throws IOException {
|
|
||||||
logger.infof("Exporting realm '%s' into file %s", realmName, this.file.getAbsolutePath());
|
logger.infof("Exporting realm '%s' into file %s", realmName, this.file.getAbsolutePath());
|
||||||
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void runExportImportTask(KeycloakSession session) throws IOException {
|
protected void runExportImportTask(KeycloakSession session) throws IOException {
|
||||||
RealmModel realm = session.realms().getRealmByName(realmName);
|
RealmModel realm = session.realms().getRealmByName(realmName);
|
||||||
|
Objects.requireNonNull(realm, "realm not found by realm name '" + realmName + "'");
|
||||||
RealmRepresentation realmRep = ExportUtils.exportRealm(session, realm, true, true);
|
RealmRepresentation realmRep = ExportUtils.exportRealm(session, realm, true, true);
|
||||||
writeToFile(realmRep);
|
writeToFile(realmRep);
|
||||||
}
|
}
|
||||||
|
@ -98,4 +110,9 @@ public class SingleFileExportProvider implements ExportProvider {
|
||||||
FileOutputStream stream = new FileOutputStream(this.file);
|
FileOutputStream stream = new FileOutputStream(this.file);
|
||||||
getObjectMapper().writeValue(stream, reps);
|
getObjectMapper().writeValue(stream, reps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ExportProvider withRealmName(String realmName) {
|
||||||
|
this.realmName = realmName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,29 +23,41 @@ import org.keycloak.exportimport.ExportProvider;
|
||||||
import org.keycloak.exportimport.ExportProviderFactory;
|
import org.keycloak.exportimport.ExportProviderFactory;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
|
|
||||||
import java.io.File;
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class SingleFileExportProviderFactory implements ExportProviderFactory {
|
public class SingleFileExportProviderFactory implements ExportProviderFactory {
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "singleFile";
|
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
|
@Override
|
||||||
public ExportProvider create(KeycloakSession session) {
|
public ExportProvider create(KeycloakSession session) {
|
||||||
String fileName = ExportImportConfig.getFile();
|
String fileName = System.getProperty(ExportImportConfig.FILE, config.get(FILE));
|
||||||
return new SingleFileExportProvider(new File(fileName));
|
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
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -56,4 +68,23 @@ public class SingleFileExportProviderFactory implements ExportProviderFactory {
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,15 @@
|
||||||
package org.keycloak.exportimport.util;
|
package org.keycloak.exportimport.util;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.exportimport.ExportImportConfig;
|
|
||||||
import org.keycloak.exportimport.ExportProvider;
|
import org.keycloak.exportimport.ExportProvider;
|
||||||
import org.keycloak.exportimport.UsersExportStrategy;
|
import org.keycloak.exportimport.UsersExportStrategy;
|
||||||
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.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.storage.UserStorageUtil;
|
import org.keycloak.storage.UserStorageUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -38,37 +37,55 @@ import java.util.stream.Collectors;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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 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
|
@Override
|
||||||
public void exportModel(KeycloakSessionFactory factory) throws IOException {
|
public void exportModel() {
|
||||||
final RealmsHolder holder = new RealmsHolder();
|
if (realmId != null) {
|
||||||
|
ServicesLogger.LOGGER.realmExportRequested(realmId);
|
||||||
KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() {
|
exportRealm(realmId);
|
||||||
|
} else {
|
||||||
@Override
|
ServicesLogger.LOGGER.fullModelExportRequested();
|
||||||
public void run(KeycloakSession session) {
|
List<RealmModel> realms = KeycloakModelUtils.runJobInTransactionWithResult(factory, session -> session.realms().getRealmsStream().collect(Collectors.toList()));
|
||||||
List<RealmModel> realms = session.realms().getRealmsStream().collect(Collectors.toList());
|
for (RealmModel realm : realms) {
|
||||||
holder.realms = realms;
|
exportRealmImpl(realm.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
for (RealmModel realm : holder.realms) {
|
|
||||||
exportRealmImpl(factory, realm.getName());
|
|
||||||
}
|
}
|
||||||
|
ServicesLogger.LOGGER.exportSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public T withRealmName(String realmName) {
|
||||||
public void exportRealm(KeycloakSessionFactory factory, String realmName) throws IOException {
|
this.realmId = realmName;
|
||||||
exportRealmImpl(factory, realmName);
|
return (T) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void exportRealmImpl(KeycloakSessionFactory factory, final String realmName) throws IOException {
|
public T withUsersPerFile(int usersPerFile) {
|
||||||
final UsersExportStrategy usersExportStrategy = ExportImportConfig.getUsersExportStrategy();
|
this.usersPerFile = usersPerFile;
|
||||||
final int usersPerFile = ExportImportConfig.getUsersPerFile();
|
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 UsersHolder usersHolder = new UsersHolder();
|
||||||
final boolean exportUsersIntoRealmFile = usersExportStrategy == UsersExportStrategy.REALM_FILE;
|
final boolean exportUsersIntoRealmFile = usersExportStrategy == UsersExportStrategy.REALM_FILE;
|
||||||
FederatedUsersHolder federatedUsersHolder = new FederatedUsersHolder();
|
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 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;
|
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 {
|
public static class UsersHolder {
|
||||||
List<UserModel> users;
|
List<UserModel> users;
|
||||||
int totalCount;
|
int totalCount;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ public enum OptionCategory {
|
||||||
VAULT("Vault", 100, ConfigSupportLevel.SUPPORTED),
|
VAULT("Vault", 100, ConfigSupportLevel.SUPPORTED),
|
||||||
LOGGING("Logging", 110, ConfigSupportLevel.SUPPORTED),
|
LOGGING("Logging", 110, ConfigSupportLevel.SUPPORTED),
|
||||||
SECURITY("Security", 120, ConfigSupportLevel.PREVIEW),
|
SECURITY("Security", 120, ConfigSupportLevel.PREVIEW),
|
||||||
|
EXPORT("Export", 130, ConfigSupportLevel.SUPPORTED),
|
||||||
GENERAL("General", 999, ConfigSupportLevel.SUPPORTED);
|
GENERAL("General", 999, ConfigSupportLevel.SUPPORTED);
|
||||||
|
|
||||||
private String heading;
|
private String heading;
|
||||||
|
|
|
@ -366,7 +366,7 @@ public final class Picocli {
|
||||||
boolean includeBuildTime = false;
|
boolean includeBuildTime = false;
|
||||||
boolean includeRuntime = 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);
|
includeBuildTime = isRebuilt() || !cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG);
|
||||||
includeRuntime = true;
|
includeRuntime = true;
|
||||||
} else if (Build.NAME.equals(command.name())) {
|
} 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) {
|
private static void addMappedOptionsToArgGroups(CommandSpec cSpec, Map<OptionCategory, List<PropertyMapper>> propertyMappers) {
|
||||||
for(OptionCategory category : OptionCategory.values()) {
|
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);
|
List<PropertyMapper> mappersInCategory = propertyMappers.get(category);
|
||||||
|
|
||||||
if (mappersInCategory == null) {
|
if (mappersInCategory == null) {
|
||||||
|
|
|
@ -19,9 +19,11 @@ package org.keycloak.quarkus.runtime.cli.command;
|
||||||
|
|
||||||
import org.keycloak.quarkus.runtime.Environment;
|
import org.keycloak.quarkus.runtime.Environment;
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import picocli.CommandLine.Option;
|
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 {
|
public abstract class AbstractExportImportCommand extends AbstractStartCommand implements Runnable {
|
||||||
|
|
||||||
private final String action;
|
private final String action;
|
||||||
|
@ -46,14 +48,21 @@ public abstract class AbstractExportImportCommand extends AbstractStartCommand i
|
||||||
public void run() {
|
public void run() {
|
||||||
System.setProperty("keycloak.migration.action", action);
|
System.setProperty("keycloak.migration.action", action);
|
||||||
|
|
||||||
if (toDir != null) {
|
if (action.equals(ACTION_IMPORT)) {
|
||||||
System.setProperty("keycloak.migration.provider", "dir");
|
if (toDir != null) {
|
||||||
System.setProperty("keycloak.migration.dir", toDir);
|
System.setProperty("keycloak.migration.provider", "dir");
|
||||||
} else if (toFile != null) {
|
System.setProperty("keycloak.migration.dir", toDir);
|
||||||
System.setProperty("keycloak.migration.provider", "singleFile");
|
} else if (toFile != null) {
|
||||||
System.setProperty("keycloak.migration.file", toFile);
|
System.setProperty("keycloak.migration.provider", "singleFile");
|
||||||
} else {
|
System.setProperty("keycloak.migration.file", toFile);
|
||||||
executionError(spec.commandLine(), "Must specify either --dir or --file options.");
|
} 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);
|
Environment.setProfile(Environment.IMPORT_EXPORT_MODE);
|
||||||
|
|
|
@ -29,40 +29,8 @@ public final class Export extends AbstractExportImportCommand implements Runnabl
|
||||||
|
|
||||||
public static final String NAME = "export";
|
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. It’s 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() {
|
public Export() {
|
||||||
super(ACTION_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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ public final class PropertyMappers {
|
||||||
MAPPERS.addAll(StoragePropertyMappers.getMappers());
|
MAPPERS.addAll(StoragePropertyMappers.getMappers());
|
||||||
MAPPERS.addAll(ClassLoaderPropertyMappers.getMappers());
|
MAPPERS.addAll(ClassLoaderPropertyMappers.getMappers());
|
||||||
MAPPERS.addAll(SecurityPropertyMappers.getMappers());
|
MAPPERS.addAll(SecurityPropertyMappers.getMappers());
|
||||||
|
MAPPERS.addAll(ExportPropertyMappers.getMappers());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
|
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
|
||||||
|
|
|
@ -27,8 +27,6 @@ import java.io.IOException;
|
||||||
*/
|
*/
|
||||||
public interface ExportProvider extends Provider {
|
public interface ExportProvider extends Provider {
|
||||||
|
|
||||||
void exportModel(KeycloakSessionFactory factory) throws IOException;
|
void exportModel() throws IOException;
|
||||||
|
|
||||||
void exportRealm(KeycloakSessionFactory factory, String realmName) throws IOException;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,24 +98,6 @@ public class ExportImportConfig {
|
||||||
System.setProperty(FILE, file);
|
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() {
|
public static Strategy getStrategy() {
|
||||||
String strategy = System.getProperty(STRATEGY, DEFAULT_STRATEGY.toString());
|
String strategy = System.getProperty(STRATEGY, DEFAULT_STRATEGY.toString());
|
||||||
return Enum.valueOf(Strategy.class, strategy);
|
return Enum.valueOf(Strategy.class, strategy);
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.exportimport;
|
||||||
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
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.KeycloakSessionTask;
|
||||||
|
@ -36,6 +37,9 @@ import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
|
@ -61,6 +65,12 @@ public class ExportImportManager {
|
||||||
String exportImportAction = ExportImportConfig.getAction();
|
String exportImportAction = ExportImportConfig.getAction();
|
||||||
|
|
||||||
if (ExportImportConfig.ACTION_EXPORT.equals(exportImportAction)) {
|
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);
|
exportProvider = session.getProvider(ExportProvider.class, providerId);
|
||||||
if (exportProvider == null) {
|
if (exportProvider == null) {
|
||||||
throw new RuntimeException("Export provider '" + providerId + "' not found");
|
throw new RuntimeException("Export provider '" + providerId + "' not found");
|
||||||
|
@ -161,16 +171,9 @@ public class ExportImportManager {
|
||||||
|
|
||||||
public void runExport() {
|
public void runExport() {
|
||||||
try {
|
try {
|
||||||
if (realmName == null) {
|
exportProvider.exportModel();
|
||||||
ServicesLogger.LOGGER.fullModelExportRequested();
|
|
||||||
exportProvider.exportModel(sessionFactory);
|
|
||||||
} else {
|
|
||||||
ServicesLogger.LOGGER.realmExportRequested(realmName);
|
|
||||||
exportProvider.exportRealm(sessionFactory, realmName);
|
|
||||||
}
|
|
||||||
ServicesLogger.LOGGER.exportSuccess();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("Failed to run export");
|
throw new RuntimeException("Failed to run export", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,7 +16,12 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.model.parameters;
|
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.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.testsuite.model.KeycloakModelParameters;
|
||||||
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
|
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
@ -32,10 +37,15 @@ import java.util.Set;
|
||||||
public class ConcurrentHashMapStorage extends KeycloakModelParameters {
|
public class ConcurrentHashMapStorage extends KeycloakModelParameters {
|
||||||
|
|
||||||
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
|
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
|
||||||
|
.add(ExportSpi.class)
|
||||||
|
.add(ClientPolicyManagerSpi.class)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
|
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
|
||||||
.add(ConcurrentHashMapStorageProviderFactory.class)
|
.add(ConcurrentHashMapStorageProviderFactory.class)
|
||||||
|
.add(SingleFileExportProviderFactory.class)
|
||||||
|
.add(DirExportProviderFactory.class)
|
||||||
|
.add(ClientPolicyManagerFactory.class)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -18,6 +18,9 @@ package org.keycloak.testsuite.model.parameters;
|
||||||
|
|
||||||
import org.keycloak.authorization.store.StoreFactorySpi;
|
import org.keycloak.authorization.store.StoreFactorySpi;
|
||||||
import org.keycloak.events.EventStoreSpi;
|
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.keys.PublicKeyStorageSpi;
|
||||||
import org.keycloak.models.DeploymentStateSpi;
|
import org.keycloak.models.DeploymentStateSpi;
|
||||||
import org.keycloak.models.SingleUseObjectProviderFactory;
|
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.singleUseObject.MapSingleUseObjectProviderFactory;
|
||||||
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
|
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
|
||||||
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
|
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.sessions.AuthenticationSessionSpi;
|
||||||
import org.keycloak.testsuite.model.KeycloakModelParameters;
|
import org.keycloak.testsuite.model.KeycloakModelParameters;
|
||||||
import org.keycloak.models.map.client.MapClientProviderFactory;
|
import org.keycloak.models.map.client.MapClientProviderFactory;
|
||||||
|
|
Loading…
Reference in a new issue