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.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 String dir;
|
||||
|
||||
private File rootDirectory;
|
||||
|
||||
private final File rootDirectory;
|
||||
|
||||
public DirExportProvider() {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
LOGGING("Logging", 110, ConfigSupportLevel.SUPPORTED),
|
||||
SECURITY("Security", 120, ConfigSupportLevel.PREVIEW),
|
||||
EXPORT("Export", 130, ConfigSupportLevel.SUPPORTED),
|
||||
GENERAL("General", 999, ConfigSupportLevel.SUPPORTED);
|
||||
|
||||
private String heading;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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. 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() {
|
||||
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(ClassLoaderPropertyMappers.getMappers());
|
||||
MAPPERS.addAll(SecurityPropertyMappers.getMappers());
|
||||
MAPPERS.addAll(ExportPropertyMappers.getMappers());
|
||||
}
|
||||
|
||||
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue