Rework the Import SPI to be configurable via the Config API
Also rework the export/import CLI for Quarkus, so that runtime options are available. Closes #17663
This commit is contained in:
parent
d29f3e4dfc
commit
251f6151e8
36 changed files with 2478 additions and 220 deletions
|
@ -28,14 +28,15 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.platform.Platform;
|
import org.keycloak.platform.Platform;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FilenameFilter;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
|
|
||||||
|
@ -44,54 +45,73 @@ import org.keycloak.services.managers.RealmManager;
|
||||||
*/
|
*/
|
||||||
public class DirImportProvider extends AbstractFileBasedImportProvider {
|
public class DirImportProvider extends AbstractFileBasedImportProvider {
|
||||||
|
|
||||||
|
private final Strategy strategy;
|
||||||
|
private final KeycloakSessionFactory factory;
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(DirImportProvider.class);
|
private static final Logger logger = Logger.getLogger(DirImportProvider.class);
|
||||||
|
|
||||||
private final File rootDirectory;
|
private File rootDirectory;
|
||||||
|
|
||||||
public DirImportProvider() {
|
private String realmName;
|
||||||
// Determine platform tmp directory
|
|
||||||
this.rootDirectory = new File(Platform.getPlatform().getTmpDirectory(), "keycloak-export");
|
|
||||||
if (!this.rootDirectory .exists()) {
|
|
||||||
throw new IllegalStateException("Directory " + this.rootDirectory + " doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.infof("Importing from directory %s", this.rootDirectory.getAbsolutePath());
|
public DirImportProvider(KeycloakSessionFactory factory, Strategy strategy) {
|
||||||
|
this.factory = factory;
|
||||||
|
this.strategy = strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DirImportProvider(File rootDirectory) {
|
public DirImportProvider withDir(String dir) {
|
||||||
this.rootDirectory = rootDirectory;
|
this.rootDirectory = new File(dir);
|
||||||
|
|
||||||
if (!this.rootDirectory.exists()) {
|
if (!this.rootDirectory.exists()) {
|
||||||
throw new IllegalStateException("Directory " + this.rootDirectory + " doesn't exist");
|
throw new IllegalStateException("Directory " + this.rootDirectory + " doesn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.infof("Importing from directory %s", this.rootDirectory.getAbsolutePath());
|
logger.infof("Importing from directory %s", this.rootDirectory.getAbsolutePath());
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public DirImportProvider withRealmName(String realmName) {
|
||||||
public void importModel(KeycloakSessionFactory factory, Strategy strategy) throws IOException {
|
this.realmName = realmName;
|
||||||
List<String> realmNames = getRealmsToImport();
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
for (String realmName : realmNames) {
|
private File getRootDirectory() {
|
||||||
importRealm(factory, realmName, strategy);
|
if (rootDirectory == null) {
|
||||||
|
this.rootDirectory = new File(Platform.getPlatform().getTmpDirectory(), "keycloak-export");
|
||||||
|
if (!this.rootDirectory.exists()) {
|
||||||
|
throw new IllegalStateException("Directory " + this.rootDirectory + " doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.infof("Importing from directory %s", this.rootDirectory.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
return rootDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isMasterRealmExported() throws IOException {
|
public void importModel() throws IOException {
|
||||||
|
if (realmName != null) {
|
||||||
|
ServicesLogger.LOGGER.realmImportRequested(realmName, strategy.toString());
|
||||||
|
importRealm(realmName, strategy);
|
||||||
|
} else {
|
||||||
|
ServicesLogger.LOGGER.fullModelImport(strategy.toString());
|
||||||
|
List<String> realmNames = getRealmsToImport();
|
||||||
|
|
||||||
|
for (String realmName : realmNames) {
|
||||||
|
importRealm(realmName, strategy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServicesLogger.LOGGER.importSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMasterRealmExported() {
|
||||||
List<String> realmNames = getRealmsToImport();
|
List<String> realmNames = getRealmsToImport();
|
||||||
return realmNames.contains(Config.getAdminRealm());
|
return realmNames.contains(Config.getAdminRealm());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getRealmsToImport() throws IOException {
|
private List<String> getRealmsToImport() {
|
||||||
File[] realmFiles = this.rootDirectory.listFiles(new FilenameFilter() {
|
File[] realmFiles = getRootDirectory().listFiles((dir, name) -> (name.endsWith("-realm.json")));
|
||||||
|
Objects.requireNonNull(realmFiles, "Directory not found: " + getRootDirectory().getName());
|
||||||
@Override
|
|
||||||
public boolean accept(File dir, String name) {
|
|
||||||
return (name.endsWith("-realm.json"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
List<String> realmNames = new ArrayList<>();
|
List<String> realmNames = new ArrayList<>();
|
||||||
for (File file : realmFiles) {
|
for (File file : realmFiles) {
|
||||||
String fileName = file.getName();
|
String fileName = file.getName();
|
||||||
|
@ -108,23 +128,12 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
|
||||||
return realmNames;
|
return realmNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void importRealm(final String realmName, final Strategy strategy) throws IOException {
|
||||||
public void importRealm(KeycloakSessionFactory factory, final String realmName, final Strategy strategy) throws IOException {
|
File realmFile = new File(getRootDirectory() + File.separator + realmName + "-realm.json");
|
||||||
File realmFile = new File(this.rootDirectory + File.separator + realmName + "-realm.json");
|
File[] userFiles = getRootDirectory().listFiles((dir, name) -> name.matches(realmName + "-users-[0-9]+\\.json"));
|
||||||
File[] userFiles = this.rootDirectory.listFiles(new FilenameFilter() {
|
Objects.requireNonNull(userFiles, "directory not found: " + getRootDirectory().getName());
|
||||||
|
File[] federatedUserFiles = getRootDirectory().listFiles((dir, name) -> name.matches(realmName + "-federated-users-[0-9]+\\.json"));
|
||||||
@Override
|
Objects.requireNonNull(federatedUserFiles, "directory not found: " + getRootDirectory().getName());
|
||||||
public boolean accept(File dir, String name) {
|
|
||||||
return name.matches(realmName + "-users-[0-9]+\\.json");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
File[] federatedUserFiles = this.rootDirectory.listFiles(new FilenameFilter() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean accept(File dir, String name) {
|
|
||||||
return name.matches(realmName + "-federated-users-[0-9]+\\.json");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Import realm first
|
// Import realm first
|
||||||
InputStream is = parseFile(realmFile);
|
InputStream is = parseFile(realmFile);
|
||||||
|
@ -134,7 +143,7 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
|
||||||
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void runExportImportTask(KeycloakSession session) throws IOException {
|
public void runExportImportTask(KeycloakSession session) {
|
||||||
boolean imported = ImportUtils.importRealm(session, realmRep, strategy, true);
|
boolean imported = ImportUtils.importRealm(session, realmRep, strategy, true);
|
||||||
realmImported.set(imported);
|
realmImported.set(imported);
|
||||||
}
|
}
|
||||||
|
@ -144,24 +153,26 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
|
||||||
if (realmImported.get()) {
|
if (realmImported.get()) {
|
||||||
// Import users
|
// Import users
|
||||||
for (final File userFile : userFiles) {
|
for (final File userFile : userFiles) {
|
||||||
final InputStream fis = parseFile(userFile);
|
try (InputStream fis = parseFile(userFile)) {
|
||||||
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 {
|
||||||
ImportUtils.importUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
|
ImportUtils.importUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
|
||||||
logger.infof("Imported users from %s", userFile.getAbsolutePath());
|
logger.infof("Imported users from %s", userFile.getAbsolutePath());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (final File userFile : federatedUserFiles) {
|
for (final File userFile : federatedUserFiles) {
|
||||||
final InputStream fis = parseFile(userFile);
|
try (InputStream fis = parseFile(userFile)) {
|
||||||
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 {
|
||||||
ImportUtils.importFederatedUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
|
ImportUtils.importFederatedUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
|
||||||
logger.infof("Imported federated users from %s", userFile.getAbsolutePath());
|
logger.infof("Imported federated users from %s", userFile.getAbsolutePath());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +181,7 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
|
||||||
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void runExportImportTask(KeycloakSession session) throws IOException {
|
public void runExportImportTask(KeycloakSession session) {
|
||||||
RealmManager realmManager = new RealmManager(session);
|
RealmManager realmManager = new RealmManager(session);
|
||||||
realmManager.setupClientServiceAccountsAndAuthorizationOnImport(realmRep, false);
|
realmManager.setupClientServiceAccountsAndAuthorizationOnImport(realmRep, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,24 +21,42 @@ import org.keycloak.Config;
|
||||||
import org.keycloak.exportimport.ExportImportConfig;
|
import org.keycloak.exportimport.ExportImportConfig;
|
||||||
import org.keycloak.exportimport.ImportProvider;
|
import org.keycloak.exportimport.ImportProvider;
|
||||||
import org.keycloak.exportimport.ImportProviderFactory;
|
import org.keycloak.exportimport.ImportProviderFactory;
|
||||||
|
import org.keycloak.exportimport.Strategy;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_STRATEGY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class DirImportProviderFactory implements ImportProviderFactory {
|
public class DirImportProviderFactory implements ImportProviderFactory {
|
||||||
|
|
||||||
|
public static final String REALM_NAME = "realmName";
|
||||||
|
public static final String DIR = "dir";
|
||||||
|
private static final String STRATEGY = "strategy";
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = DirExportProviderFactory.PROVIDER_ID;
|
||||||
|
|
||||||
|
private Config.Scope config;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ImportProvider create(KeycloakSession session) {
|
public ImportProvider create(KeycloakSession session) {
|
||||||
String dir = ExportImportConfig.getDir();
|
Strategy strategy = Enum.valueOf(Strategy.class, System.getProperty(ExportImportConfig.STRATEGY, config.get(STRATEGY, DEFAULT_STRATEGY.toString())));
|
||||||
return dir!=null ? new DirImportProvider(new File(dir)) : new DirImportProvider();
|
String realmName = System.getProperty(ExportImportConfig.REALM_NAME, config.get(REALM_NAME));
|
||||||
|
String dir = System.getProperty(ExportImportConfig.DIR, config.get(DIR));
|
||||||
|
return new DirImportProvider(session.getKeycloakSessionFactory(), strategy)
|
||||||
|
.withDir(dir)
|
||||||
|
.withRealmName(realmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -52,6 +70,30 @@ public class DirImportProviderFactory implements ImportProviderFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return DirExportProviderFactory.PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 import from")
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.property()
|
||||||
|
.name(STRATEGY)
|
||||||
|
.type("string")
|
||||||
|
.helpText("Strategy for import: " + Strategy.IGNORE_EXISTING.name() + ", " + Strategy.OVERWRITE_EXISTING)
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,25 +40,28 @@ import java.util.Map;
|
||||||
public class SingleFileImportProvider extends AbstractFileBasedImportProvider {
|
public class SingleFileImportProvider extends AbstractFileBasedImportProvider {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(SingleFileImportProvider.class);
|
private static final Logger logger = Logger.getLogger(SingleFileImportProvider.class);
|
||||||
|
private final KeycloakSessionFactory factory;
|
||||||
|
|
||||||
private File file;
|
private final File file;
|
||||||
|
private final Strategy strategy;
|
||||||
|
|
||||||
// Allows to cache representation per provider to avoid parsing them twice
|
// Allows to cache representation per provider to avoid parsing them twice
|
||||||
protected Map<String, RealmRepresentation> realmReps;
|
protected Map<String, RealmRepresentation> realmReps;
|
||||||
|
|
||||||
public SingleFileImportProvider(File file) {
|
public SingleFileImportProvider(KeycloakSessionFactory factory, File file, Strategy strategy) {
|
||||||
|
this.factory = factory;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
|
this.strategy = strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void importModel() throws IOException {
|
||||||
public void importModel(KeycloakSessionFactory factory, final Strategy strategy) throws IOException {
|
|
||||||
logger.infof("Full importing from file %s", this.file.getAbsolutePath());
|
logger.infof("Full importing from file %s", this.file.getAbsolutePath());
|
||||||
checkRealmReps();
|
checkRealmReps();
|
||||||
|
|
||||||
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void runExportImportTask(KeycloakSession session) throws IOException {
|
protected void runExportImportTask(KeycloakSession session) {
|
||||||
ImportUtils.importRealms(session, realmReps.values(), strategy);
|
ImportUtils.importRealms(session, realmReps.values(), strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,12 +81,6 @@ public class SingleFileImportProvider extends AbstractFileBasedImportProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void importRealm(KeycloakSessionFactory factory, String realmName, Strategy strategy) throws IOException {
|
|
||||||
// TODO: import just that single realm in case that file contains many realms?
|
|
||||||
importModel(factory, strategy);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
|
||||||
|
|
|
@ -21,27 +21,44 @@ import org.keycloak.Config;
|
||||||
import org.keycloak.exportimport.ExportImportConfig;
|
import org.keycloak.exportimport.ExportImportConfig;
|
||||||
import org.keycloak.exportimport.ImportProvider;
|
import org.keycloak.exportimport.ImportProvider;
|
||||||
import org.keycloak.exportimport.ImportProviderFactory;
|
import org.keycloak.exportimport.ImportProviderFactory;
|
||||||
|
import org.keycloak.exportimport.Strategy;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_STRATEGY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class SingleFileImportProviderFactory implements ImportProviderFactory {
|
public class SingleFileImportProviderFactory implements ImportProviderFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = SingleFileExportProviderFactory.PROVIDER_ID;
|
||||||
|
|
||||||
|
public static final String REALM_NAME = "realmName";
|
||||||
|
public static final String STRATEGY = "strategy";
|
||||||
|
|
||||||
|
public static final String FILE = "file";
|
||||||
|
|
||||||
|
private Config.Scope config;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ImportProvider create(KeycloakSession session) {
|
public ImportProvider create(KeycloakSession session) {
|
||||||
String fileName = ExportImportConfig.getFile();
|
Strategy strategy = Enum.valueOf(Strategy.class, System.getProperty(ExportImportConfig.STRATEGY, config.get(STRATEGY, DEFAULT_STRATEGY.toString())));
|
||||||
|
String fileName = System.getProperty(ExportImportConfig.FILE, config.get(FILE));
|
||||||
if (fileName == null) {
|
if (fileName == null) {
|
||||||
throw new IllegalArgumentException("Property " + ExportImportConfig.FILE + " needs to be provided!");
|
throw new IllegalArgumentException("Property " + FILE + " needs to be provided!");
|
||||||
}
|
}
|
||||||
return new SingleFileImportProvider(new File(fileName));
|
return new SingleFileImportProvider(session.getKeycloakSessionFactory(), new File(fileName), strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -55,6 +72,30 @@ public class SingleFileImportProviderFactory implements ImportProviderFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return SingleFileExportProviderFactory.PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 import from")
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.property()
|
||||||
|
.name(STRATEGY)
|
||||||
|
.type("string")
|
||||||
|
.helpText("Strategy for import: " + Strategy.IGNORE_EXISTING.name() + ", " + Strategy.OVERWRITE_EXISTING)
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,13 @@ public class ExportOptions {
|
||||||
|
|
||||||
public static final Option<String> FILE = new OptionBuilder<>("file", String.class)
|
public static final Option<String> FILE = new OptionBuilder<>("file", String.class)
|
||||||
.category(OptionCategory.EXPORT)
|
.category(OptionCategory.EXPORT)
|
||||||
.hidden() // hidden for now until we refactor the import command
|
.description("Set the path to a file that will be created with the exported data.")
|
||||||
.buildTime(false)
|
.buildTime(false)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
public static final Option<String> DIR = new OptionBuilder<>("dir", String.class)
|
public static final Option<String> DIR = new OptionBuilder<>("dir", String.class)
|
||||||
.category(OptionCategory.EXPORT)
|
.category(OptionCategory.EXPORT)
|
||||||
.hidden() // hidden for now until we refactor the import command
|
.description("Set the path to a directory where files will be created with the exported data.")
|
||||||
.buildTime(false)
|
.buildTime(false)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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 ImportOptions {
|
||||||
|
|
||||||
|
public static final Option<String> FILE = new OptionBuilder<>("file", String.class)
|
||||||
|
.category(OptionCategory.IMPORT)
|
||||||
|
.description("Set the path to a file that will be read.")
|
||||||
|
.buildTime(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final Option<String> DIR = new OptionBuilder<>("dir", String.class)
|
||||||
|
.category(OptionCategory.IMPORT)
|
||||||
|
.description("Set the path to a directory where files will be read from.")
|
||||||
|
.buildTime(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final Option<Boolean> OVERRIDE = new OptionBuilder<>("override", Boolean.class)
|
||||||
|
.category(OptionCategory.IMPORT)
|
||||||
|
.defaultValue(Boolean.TRUE)
|
||||||
|
.description("Set if existing data should be overwritten. If set to false, data will be ignored.")
|
||||||
|
.buildTime(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
|
@ -16,9 +16,10 @@ public enum OptionCategory {
|
||||||
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),
|
EXPORT("Export", 130, ConfigSupportLevel.SUPPORTED),
|
||||||
|
IMPORT("Import", 140, ConfigSupportLevel.SUPPORTED),
|
||||||
GENERAL("General", 999, ConfigSupportLevel.SUPPORTED);
|
GENERAL("General", 999, ConfigSupportLevel.SUPPORTED);
|
||||||
|
|
||||||
private String heading;
|
private final String heading;
|
||||||
//Categories with a lower number are shown before groups with a higher number
|
//Categories with a lower number are shown before groups with a higher number
|
||||||
private final int order;
|
private final int order;
|
||||||
private final ConfigSupportLevel supportLevel;
|
private final ConfigSupportLevel supportLevel;
|
||||||
|
|
|
@ -50,6 +50,7 @@ import java.util.stream.Collectors;
|
||||||
import org.eclipse.microprofile.config.spi.ConfigSource;
|
import org.eclipse.microprofile.config.spi.ConfigSource;
|
||||||
import org.keycloak.config.MultiOption;
|
import org.keycloak.config.MultiOption;
|
||||||
import org.keycloak.config.OptionCategory;
|
import org.keycloak.config.OptionCategory;
|
||||||
|
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
|
||||||
import org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand;
|
import org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand;
|
||||||
import org.keycloak.quarkus.runtime.cli.command.Build;
|
import org.keycloak.quarkus.runtime.cli.command.Build;
|
||||||
import org.keycloak.quarkus.runtime.cli.command.Export;
|
import org.keycloak.quarkus.runtime.cli.command.Export;
|
||||||
|
@ -347,7 +348,7 @@ public final class Picocli {
|
||||||
|
|
||||||
if (isRebuildCheck()) {
|
if (isRebuildCheck()) {
|
||||||
// build command should be available when running re-aug
|
// build command should be available when running re-aug
|
||||||
addCommandOptions(cliArgs, spec.subcommands().get(Build.NAME).getCommandSpec());
|
addCommandOptions(cliArgs, spec.subcommands().get(Build.NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandLine cmd = new CommandLine(spec);
|
CommandLine cmd = new CommandLine(spec);
|
||||||
|
@ -361,16 +362,22 @@ public final class Picocli {
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addCommandOptions(List<String> cliArgs, CommandSpec command) {
|
private static void addCommandOptions(List<String> cliArgs, CommandLine command) {
|
||||||
if (command != null) {
|
if (command != null) {
|
||||||
boolean includeBuildTime = false;
|
boolean includeBuildTime = false;
|
||||||
boolean includeRuntime = false;
|
boolean includeRuntime = false;
|
||||||
|
|
||||||
if (Start.NAME.equals(command.name()) || StartDev.NAME.equals(command.name()) || Export.NAME.equals(command.name())) {
|
if (command.getCommand() instanceof AbstractCommand) {
|
||||||
|
AbstractCommand abstractCommand = command.getCommand();
|
||||||
|
includeRuntime = abstractCommand.includeRuntime();
|
||||||
|
includeBuildTime = abstractCommand.includeBuildTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!includeBuildTime && !includeRuntime) {
|
||||||
|
return;
|
||||||
|
} else if (includeRuntime && !includeBuildTime && (Start.NAME.equals(command.getCommandName())) || StartDev.NAME.equals(command.getCommandName())) {
|
||||||
includeBuildTime = isRebuilt() || !cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG);
|
includeBuildTime = isRebuilt() || !cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG);
|
||||||
includeRuntime = true;
|
} else if (includeBuildTime && !includeRuntime) {
|
||||||
} else if (Build.NAME.equals(command.name())) {
|
|
||||||
includeBuildTime = true;
|
|
||||||
includeRuntime = isRebuildCheck();
|
includeRuntime = isRebuildCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,19 +385,19 @@ public final class Picocli {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CommandSpec getCurrentCommandSpec(List<String> cliArgs, CommandSpec spec) {
|
private static CommandLine getCurrentCommandSpec(List<String> cliArgs, CommandSpec spec) {
|
||||||
for (String arg : cliArgs) {
|
for (String arg : cliArgs) {
|
||||||
CommandLine command = spec.subcommands().get(arg);
|
CommandLine command = spec.subcommands().get(arg);
|
||||||
|
|
||||||
if (command != null) {
|
if (command != null) {
|
||||||
return command.getCommandSpec();
|
return command;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addOptionsToCli(CommandSpec commandSpec, boolean includeBuildTime, boolean includeRuntime) {
|
private static void addOptionsToCli(CommandLine commandLine, boolean includeBuildTime, boolean includeRuntime) {
|
||||||
Map<OptionCategory, List<PropertyMapper>> mappers = new EnumMap<>(OptionCategory.class);
|
Map<OptionCategory, List<PropertyMapper>> mappers = new EnumMap<>(OptionCategory.class);
|
||||||
|
|
||||||
if (includeRuntime) {
|
if (includeRuntime) {
|
||||||
|
@ -408,23 +415,12 @@ public final class Picocli {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addMappedOptionsToArgGroups(commandSpec, mappers);
|
addMappedOptionsToArgGroups(commandLine, mappers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addMappedOptionsToArgGroups(CommandSpec cSpec, Map<OptionCategory, List<PropertyMapper>> propertyMappers) {
|
private static void addMappedOptionsToArgGroups(CommandLine commandLine, Map<OptionCategory, List<PropertyMapper>> propertyMappers) {
|
||||||
for(OptionCategory category : OptionCategory.values()) {
|
CommandSpec cSpec = commandLine.getCommandSpec();
|
||||||
if (cSpec.name().equals(Export.NAME)) {
|
for(OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) {
|
||||||
// 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,10 +19,14 @@ package org.keycloak.quarkus.runtime.cli.command;
|
||||||
|
|
||||||
import static org.keycloak.quarkus.runtime.Messages.cliExecutionError;
|
import static org.keycloak.quarkus.runtime.Messages.cliExecutionError;
|
||||||
|
|
||||||
|
import org.keycloak.config.OptionCategory;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
import picocli.CommandLine.Model.CommandSpec;
|
import picocli.CommandLine.Model.CommandSpec;
|
||||||
import picocli.CommandLine.Spec;
|
import picocli.CommandLine.Spec;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class AbstractCommand {
|
public abstract class AbstractCommand {
|
||||||
|
|
||||||
@Spec
|
@Spec
|
||||||
|
@ -35,4 +39,25 @@ public abstract class AbstractCommand {
|
||||||
protected void executionError(CommandLine cmd, String message, Throwable cause) {
|
protected void executionError(CommandLine cmd, String message, Throwable cause) {
|
||||||
cliExecutionError(cmd, message, cause);
|
cliExecutionError(cmd, message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this command should include runtime options for the CLI.
|
||||||
|
*/
|
||||||
|
public boolean includeRuntime() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this command should include build time options for the CLI.
|
||||||
|
*/
|
||||||
|
public boolean includeBuildTime() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all option categories which are available for this command.
|
||||||
|
*/
|
||||||
|
public List<OptionCategory> getOptionCategories() {
|
||||||
|
return Arrays.asList(OptionCategory.values());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,28 +17,19 @@
|
||||||
|
|
||||||
package org.keycloak.quarkus.runtime.cli.command;
|
package org.keycloak.quarkus.runtime.cli.command;
|
||||||
|
|
||||||
|
import org.keycloak.config.OptionCategory;
|
||||||
import org.keycloak.quarkus.runtime.Environment;
|
import org.keycloak.quarkus.runtime.Environment;
|
||||||
|
import picocli.CommandLine;
|
||||||
|
|
||||||
import picocli.CommandLine.Option;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
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;
|
||||||
|
|
||||||
@Option(names = "--dir",
|
@CommandLine.Mixin
|
||||||
arity = "1",
|
HelpAllMixin helpAllMixin;
|
||||||
description = "Set the path to a directory where files will be read from when importing or created with the exported data.",
|
|
||||||
paramLabel = "<path>")
|
|
||||||
String toDir;
|
|
||||||
|
|
||||||
@Option(names = "--file",
|
|
||||||
arity = "1",
|
|
||||||
description = "Set the path to a file that will be read when importing or created with the exported data.",
|
|
||||||
paramLabel = "<path>")
|
|
||||||
String toFile;
|
|
||||||
|
|
||||||
protected AbstractExportImportCommand(String action) {
|
protected AbstractExportImportCommand(String action) {
|
||||||
this.action = action;
|
this.action = action;
|
||||||
|
@ -48,25 +39,23 @@ 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 (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);
|
Environment.setProfile(Environment.IMPORT_EXPORT_MODE);
|
||||||
|
|
||||||
super.run();
|
super.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OptionCategory> getOptionCategories() {
|
||||||
|
return super.getOptionCategories().stream().filter(optionCategory ->
|
||||||
|
optionCategory != OptionCategory.HTTP &&
|
||||||
|
optionCategory != OptionCategory.PROXY &&
|
||||||
|
optionCategory != OptionCategory.HOSTNAME &&
|
||||||
|
optionCategory != OptionCategory.METRICS &&
|
||||||
|
optionCategory != OptionCategory.HEALTH).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean includeRuntime() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import static org.keycloak.quarkus.runtime.Environment.isDevMode;
|
||||||
import static org.keycloak.quarkus.runtime.cli.Picocli.println;
|
import static org.keycloak.quarkus.runtime.cli.Picocli.println;
|
||||||
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.getAllCliArgs;
|
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.getAllCliArgs;
|
||||||
|
|
||||||
|
import org.keycloak.config.OptionCategory;
|
||||||
import org.keycloak.quarkus.runtime.Environment;
|
import org.keycloak.quarkus.runtime.Environment;
|
||||||
import org.keycloak.quarkus.runtime.Messages;
|
import org.keycloak.quarkus.runtime.Messages;
|
||||||
|
|
||||||
|
@ -32,6 +33,9 @@ import io.quarkus.runtime.configuration.ProfileManager;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
import picocli.CommandLine.Command;
|
import picocli.CommandLine.Command;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Command(name = Build.NAME,
|
@Command(name = Build.NAME,
|
||||||
header = "Creates a new and optimized server image.",
|
header = "Creates a new and optimized server image.",
|
||||||
description = {
|
description = {
|
||||||
|
@ -81,6 +85,15 @@ public final class Build extends AbstractCommand implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean includeBuildTime() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OptionCategory> getOptionCategories() {
|
||||||
|
return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private void exitWithErrorIfDevProfileIsSetAndNotStartDev() {
|
private void exitWithErrorIfDevProfileIsSetAndNotStartDev() {
|
||||||
if (Environment.isDevProfile() && !getAllCliArgs().contains(StartDev.NAME)) {
|
if (Environment.isDevProfile() && !getAllCliArgs().contains(StartDev.NAME)) {
|
||||||
executionError(spec.commandLine(), Messages.devProfileNotAllowedError(NAME));
|
executionError(spec.commandLine(), Messages.devProfileNotAllowedError(NAME));
|
||||||
|
|
|
@ -19,8 +19,11 @@ package org.keycloak.quarkus.runtime.cli.command;
|
||||||
|
|
||||||
import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
|
import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
|
||||||
|
|
||||||
|
import org.keycloak.config.OptionCategory;
|
||||||
import picocli.CommandLine.Command;
|
import picocli.CommandLine.Command;
|
||||||
import picocli.CommandLine.Option;
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Command(name = Export.NAME,
|
@Command(name = Export.NAME,
|
||||||
header = "Export data from realms to a file or directory.",
|
header = "Export data from realms to a file or directory.",
|
||||||
|
@ -33,4 +36,10 @@ public final class Export extends AbstractExportImportCommand implements Runnabl
|
||||||
super(ACTION_EXPORT);
|
super(ACTION_EXPORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OptionCategory> getOptionCategories() {
|
||||||
|
return super.getOptionCategories().stream().filter(optionCategory ->
|
||||||
|
optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,12 @@
|
||||||
package org.keycloak.quarkus.runtime.cli.command;
|
package org.keycloak.quarkus.runtime.cli.command;
|
||||||
|
|
||||||
import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT;
|
import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT;
|
||||||
import static org.keycloak.exportimport.Strategy.IGNORE_EXISTING;
|
|
||||||
import static org.keycloak.exportimport.Strategy.OVERWRITE_EXISTING;
|
|
||||||
|
|
||||||
|
import org.keycloak.config.OptionCategory;
|
||||||
import picocli.CommandLine.Command;
|
import picocli.CommandLine.Command;
|
||||||
import picocli.CommandLine.Option;
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Command(name = Import.NAME,
|
@Command(name = Import.NAME,
|
||||||
header = "Import data from a directory or a file.",
|
header = "Import data from a directory or a file.",
|
||||||
|
@ -31,19 +32,14 @@ public final class Import extends AbstractExportImportCommand implements Runnabl
|
||||||
|
|
||||||
public static final String NAME = "import";
|
public static final String NAME = "import";
|
||||||
|
|
||||||
@Option(names = "--override",
|
|
||||||
arity = "1",
|
|
||||||
description = "Set if existing data should be skipped or overridden.",
|
|
||||||
paramLabel = "false",
|
|
||||||
defaultValue = "true")
|
|
||||||
boolean override;
|
|
||||||
|
|
||||||
public Import() {
|
public Import() {
|
||||||
super(ACTION_IMPORT);
|
super(ACTION_IMPORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doBeforeRun() {
|
public List<OptionCategory> getOptionCategories() {
|
||||||
System.setProperty("keycloak.migration.strategy", override ? OVERWRITE_EXISTING.name() : IGNORE_EXISTING.name());
|
return super.getOptionCategories().stream().filter(optionCategory ->
|
||||||
|
optionCategory != OptionCategory.EXPORT).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import static org.keycloak.quarkus.runtime.Environment.setProfile;
|
||||||
import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL;
|
import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL;
|
||||||
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty;
|
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty;
|
||||||
|
|
||||||
|
import org.keycloak.config.OptionCategory;
|
||||||
import org.keycloak.quarkus.runtime.Environment;
|
import org.keycloak.quarkus.runtime.Environment;
|
||||||
import org.keycloak.quarkus.runtime.Messages;
|
import org.keycloak.quarkus.runtime.Messages;
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ import picocli.CommandLine.Command;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Command(name = Start.NAME,
|
@Command(name = Start.NAME,
|
||||||
header = "Start the server.",
|
header = "Start the server.",
|
||||||
|
@ -85,4 +87,13 @@ public final class Start extends AbstractStartCommand implements Runnable {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<OptionCategory> getOptionCategories() {
|
||||||
|
return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean includeRuntime() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.quarkus.runtime.cli.command;
|
package org.keycloak.quarkus.runtime.cli.command;
|
||||||
|
|
||||||
|
import org.keycloak.config.OptionCategory;
|
||||||
import org.keycloak.quarkus.runtime.Environment;
|
import org.keycloak.quarkus.runtime.Environment;
|
||||||
|
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
@ -24,6 +25,9 @@ import picocli.CommandLine.Command;
|
||||||
import picocli.CommandLine.Mixin;
|
import picocli.CommandLine.Mixin;
|
||||||
import picocli.CommandLine.Option;
|
import picocli.CommandLine.Option;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Command(name = StartDev.NAME,
|
@Command(name = StartDev.NAME,
|
||||||
header = "Start the server in development mode.",
|
header = "Start the server in development mode.",
|
||||||
description = {
|
description = {
|
||||||
|
@ -48,4 +52,14 @@ public final class StartDev extends AbstractStartCommand implements Runnable {
|
||||||
protected void doBeforeRun() {
|
protected void doBeforeRun() {
|
||||||
Environment.forceDevProfile();
|
Environment.forceDevProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OptionCategory> getOptionCategories() {
|
||||||
|
return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean includeRuntime() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,11 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
|
||||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||||
import io.smallrye.config.ConfigValue;
|
import io.smallrye.config.ConfigValue;
|
||||||
import org.keycloak.config.ExportOptions;
|
import org.keycloak.config.ExportOptions;
|
||||||
|
import picocli.CommandLine;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
|
||||||
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
|
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
|
||||||
|
|
||||||
final class ExportPropertyMappers {
|
final class ExportPropertyMappers {
|
||||||
|
@ -65,6 +67,10 @@ final class ExportPropertyMappers {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<String> transformExporter(Optional<String> option, ConfigSourceInterceptorContext context) {
|
private static Optional<String> transformExporter(Optional<String> option, ConfigSourceInterceptorContext context) {
|
||||||
|
ConfigValue exporter = context.proceed("kc.spi-export-exporter");
|
||||||
|
if (exporter != null) {
|
||||||
|
return Optional.of(exporter.getValue());
|
||||||
|
}
|
||||||
if (option.isPresent()) {
|
if (option.isPresent()) {
|
||||||
return Optional.of("singleFile");
|
return Optional.of("singleFile");
|
||||||
}
|
}
|
||||||
|
@ -72,6 +78,13 @@ final class ExportPropertyMappers {
|
||||||
if (dirConfigValue != null && dirConfigValue.getValue() != null) {
|
if (dirConfigValue != null && dirConfigValue.getValue() != null) {
|
||||||
return Optional.of("dir");
|
return Optional.of("dir");
|
||||||
}
|
}
|
||||||
|
ConfigValue dirValue = context.proceed("kc.dir");
|
||||||
|
if (dirValue != null && dirValue.getValue() != null) {
|
||||||
|
return Optional.of("dir");
|
||||||
|
}
|
||||||
|
if (System.getProperty(PROVIDER) == null) {
|
||||||
|
throw new CommandLine.PicocliException("Must specify either --dir or --file options.");
|
||||||
|
}
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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.ImportOptions;
|
||||||
|
import org.keycloak.exportimport.Strategy;
|
||||||
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
|
||||||
|
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
|
||||||
|
|
||||||
|
final class ImportPropertyMappers {
|
||||||
|
|
||||||
|
private ImportPropertyMappers() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PropertyMapper<?>[] getMappers() {
|
||||||
|
return new PropertyMapper[] {
|
||||||
|
fromOption(ImportOptions.FILE)
|
||||||
|
.to("kc.spi-import-importer")
|
||||||
|
.transformer(ImportPropertyMappers::transformImporter)
|
||||||
|
.paramLabel("file")
|
||||||
|
.build(),
|
||||||
|
fromOption(ImportOptions.FILE)
|
||||||
|
.to("kc.spi-import-single-file-file")
|
||||||
|
.paramLabel("file")
|
||||||
|
.build(),
|
||||||
|
fromOption(ImportOptions.DIR)
|
||||||
|
.to("kc.spi-import-dir-dir")
|
||||||
|
.paramLabel("dir")
|
||||||
|
.build(),
|
||||||
|
fromOption(ImportOptions.OVERRIDE)
|
||||||
|
.to("kc.spi-import-single-file-strategy")
|
||||||
|
.transformer(ImportPropertyMappers::transformOverride)
|
||||||
|
.build(),
|
||||||
|
fromOption(ImportOptions.OVERRIDE)
|
||||||
|
.to("kc.spi-import-dir-strategy")
|
||||||
|
.transformer(ImportPropertyMappers::transformOverride)
|
||||||
|
.build(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<String> transformOverride(Optional<String> option, ConfigSourceInterceptorContext context) {
|
||||||
|
if (option.isPresent() && Boolean.parseBoolean(option.get())) {
|
||||||
|
return Optional.of(Strategy.OVERWRITE_EXISTING.name());
|
||||||
|
} else {
|
||||||
|
return Optional.of(Strategy.IGNORE_EXISTING.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<String> transformImporter(Optional<String> option, ConfigSourceInterceptorContext context) {
|
||||||
|
ConfigValue importer = context.proceed("kc.spi-import-importer");
|
||||||
|
if (importer != null) {
|
||||||
|
return Optional.of(importer.getValue());
|
||||||
|
}
|
||||||
|
if (option.isPresent()) {
|
||||||
|
return Optional.of("singleFile");
|
||||||
|
}
|
||||||
|
ConfigValue dirConfigValue = context.proceed("kc.spi-import-dir-dir");
|
||||||
|
if (dirConfigValue != null && dirConfigValue.getValue() != null) {
|
||||||
|
return Optional.of("dir");
|
||||||
|
}
|
||||||
|
ConfigValue dirValue = context.proceed("kc.dir");
|
||||||
|
if (dirConfigValue != null && dirValue.getValue() != null) {
|
||||||
|
return Optional.of("dir");
|
||||||
|
}
|
||||||
|
if (System.getProperty(PROVIDER) == null) {
|
||||||
|
throw new CommandLine.PicocliException("Must specify either --dir or --file options.");
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ public final class PropertyMappers {
|
||||||
MAPPERS.addAll(ClassLoaderPropertyMappers.getMappers());
|
MAPPERS.addAll(ClassLoaderPropertyMappers.getMappers());
|
||||||
MAPPERS.addAll(SecurityPropertyMappers.getMappers());
|
MAPPERS.addAll(SecurityPropertyMappers.getMappers());
|
||||||
MAPPERS.addAll(ExportPropertyMappers.getMappers());
|
MAPPERS.addAll(ExportPropertyMappers.getMappers());
|
||||||
|
MAPPERS.addAll(ImportPropertyMappers.getMappers());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
|
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
|
||||||
|
|
|
@ -38,6 +38,6 @@ public class ExportDistTest {
|
||||||
cliResult.assertNoMessage("Listening on:");
|
cliResult.assertNoMessage("Listening on:");
|
||||||
|
|
||||||
cliResult = dist.run("export", "--realm=master");
|
cliResult = dist.run("export", "--realm=master");
|
||||||
cliResult.assertError("Must specify either --dir or --file options.");
|
cliResult.assertMessage("Must specify either --dir or --file options.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ import org.keycloak.it.junit5.extension.DistributionTest;
|
||||||
import org.keycloak.it.junit5.extension.RawDistOnly;
|
import org.keycloak.it.junit5.extension.RawDistOnly;
|
||||||
import org.keycloak.it.utils.KeycloakDistribution;
|
import org.keycloak.it.utils.KeycloakDistribution;
|
||||||
import org.keycloak.quarkus.runtime.cli.command.Build;
|
import org.keycloak.quarkus.runtime.cli.command.Build;
|
||||||
|
import org.keycloak.quarkus.runtime.cli.command.Export;
|
||||||
|
import org.keycloak.quarkus.runtime.cli.command.Import;
|
||||||
import org.keycloak.quarkus.runtime.cli.command.Start;
|
import org.keycloak.quarkus.runtime.cli.command.Start;
|
||||||
import org.keycloak.quarkus.runtime.cli.command.StartDev;
|
import org.keycloak.quarkus.runtime.cli.command.StartDev;
|
||||||
|
|
||||||
|
@ -110,6 +112,34 @@ public class HelpCommandDistTest {
|
||||||
cliResult.assertHelp();
|
cliResult.assertHelp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Launch({ Export.NAME, "--help" })
|
||||||
|
void testExportHelp(LaunchResult result) {
|
||||||
|
CLIResult cliResult = (CLIResult) result;
|
||||||
|
cliResult.assertHelp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Launch({ Export.NAME, "--help-all" })
|
||||||
|
void testExportHelpAll(LaunchResult result) {
|
||||||
|
CLIResult cliResult = (CLIResult) result;
|
||||||
|
cliResult.assertHelp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Launch({ Import.NAME, "--help" })
|
||||||
|
void testImportHelp(LaunchResult result) {
|
||||||
|
CLIResult cliResult = (CLIResult) result;
|
||||||
|
cliResult.assertHelp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Launch({ Import.NAME, "--help-all" })
|
||||||
|
void testImportHelpAll(LaunchResult result) {
|
||||||
|
CLIResult cliResult = (CLIResult) result;
|
||||||
|
cliResult.assertHelp();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHelpDoesNotStartReAugJvm(KeycloakDistribution dist) {
|
public void testHelpDoesNotStartReAugJvm(KeycloakDistribution dist) {
|
||||||
for (String helpCmd : List.of("-h", "--help", "--help-all")) {
|
for (String helpCmd : List.of("-h", "--help", "--help-all")) {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
HelpCommandDistTest.*.received.txt
|
|
@ -0,0 +1,107 @@
|
||||||
|
Export data from realms to a file or directory.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
kc.sh export [OPTIONS]
|
||||||
|
|
||||||
|
Export data from realms to a file or directory.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help This help message.
|
||||||
|
--help-all This same help message but with additional options.
|
||||||
|
|
||||||
|
Database:
|
||||||
|
|
||||||
|
--db-password <password>
|
||||||
|
The password of the database user.
|
||||||
|
--db-pool-initial-size <size>
|
||||||
|
The initial size of the connection pool.
|
||||||
|
--db-pool-max-size <size>
|
||||||
|
The maximum size of the connection pool. Default: 100.
|
||||||
|
--db-pool-min-size <size>
|
||||||
|
The minimal size of the connection pool.
|
||||||
|
--db-schema <schema> The database schema to be used.
|
||||||
|
--db-url <jdbc-url> The full database JDBC URL. If not provided, a default URL is set based on the
|
||||||
|
selected database vendor. For instance, if using 'postgres', the default
|
||||||
|
JDBC URL would be 'jdbc:postgresql://localhost/keycloak'.
|
||||||
|
--db-url-database <dbname>
|
||||||
|
Sets the database name of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-url-host <hostname>
|
||||||
|
Sets the hostname of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-url-port <port> Sets the port of the default JDBC URL of the chosen vendor. If the `db-url`
|
||||||
|
option is set, this option is ignored.
|
||||||
|
--db-url-properties <properties>
|
||||||
|
Sets the properties of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-username <username>
|
||||||
|
The username of the database user.
|
||||||
|
|
||||||
|
Vault:
|
||||||
|
|
||||||
|
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
|
||||||
|
given directory.
|
||||||
|
|
||||||
|
Logging:
|
||||||
|
|
||||||
|
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
|
||||||
|
are: console, file, gelf. Default: console.
|
||||||
|
--log-console-color <true|false>
|
||||||
|
Enable or disable colors when logging to console. Default: false.
|
||||||
|
--log-console-format <format>
|
||||||
|
The format of unstructured console log entries. If the format has spaces in
|
||||||
|
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
|
||||||
|
-5p [%c] (%t) %s%e%n.
|
||||||
|
--log-console-output <output>
|
||||||
|
Set the log output to JSON or default (plain) unstructured logging. Possible
|
||||||
|
values are: default, json. Default: default.
|
||||||
|
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
|
||||||
|
--log-file-format <format>
|
||||||
|
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
|
||||||
|
SSS} %-5p [%c] (%t) %s%e%n.
|
||||||
|
--log-file-output <output>
|
||||||
|
Set the log output to JSON or default (plain) unstructured logging. Possible
|
||||||
|
values are: default, json. Default: default.
|
||||||
|
--log-gelf-facility <name>
|
||||||
|
The facility (name of the process) that sends the message. Default: keycloak.
|
||||||
|
--log-gelf-host <hostname>
|
||||||
|
Hostname of the Logstash or Graylog Host. By default UDP is used, prefix the
|
||||||
|
host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' Default:
|
||||||
|
localhost.
|
||||||
|
--log-gelf-include-location <true|false>
|
||||||
|
Include source code location. Default: true.
|
||||||
|
--log-gelf-include-message-parameters <true|false>
|
||||||
|
Include message parameters from the log event. Default: true.
|
||||||
|
--log-gelf-include-stack-trace <true|false>
|
||||||
|
If set to true, occuring stack traces are included in the 'StackTrace' field
|
||||||
|
in the GELF output. Default: true.
|
||||||
|
--log-gelf-level <level>
|
||||||
|
The log level specifying which message levels will be logged by the GELF
|
||||||
|
logger. Message levels lower than this value will be discarded. Default:
|
||||||
|
INFO.
|
||||||
|
--log-gelf-max-message-size <size>
|
||||||
|
Maximum message size (in bytes). If the message size is exceeded, GELF will
|
||||||
|
submit the message in multiple chunks. Default: 8192.
|
||||||
|
--log-gelf-port <port>
|
||||||
|
The port the Logstash or Graylog Host is called on. Default: 12201.
|
||||||
|
--log-gelf-timestamp-format <pattern>
|
||||||
|
Set the format for the GELF timestamp field. Uses Java SimpleDateFormat
|
||||||
|
pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
|
||||||
|
--log-level <category:level>
|
||||||
|
The log level of the root category or a comma-separated list of individual
|
||||||
|
categories and their levels. For the root category, you don't need to
|
||||||
|
specify a category. Default: info.
|
||||||
|
|
||||||
|
Export:
|
||||||
|
|
||||||
|
--dir <dir> Set the path to a directory where files will be created with the exported data.
|
||||||
|
--file <file> Set the path to a file that will be created with the exported data.
|
||||||
|
--realm <realm> Set the name of the realm to export. If not set, all realms are going to be
|
||||||
|
exported.
|
||||||
|
--users <strategy> Set how users should be exported. Possible values are: skip, realm_file,
|
||||||
|
same_file, different_files. Default: different_files.
|
||||||
|
--users-per-file <number>
|
||||||
|
Set the number of users per file. It is used only if 'users' is set to
|
||||||
|
'different_files'. Default: 50.
|
|
@ -0,0 +1,126 @@
|
||||||
|
Export data from realms to a file or directory.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
kc.sh export [OPTIONS]
|
||||||
|
|
||||||
|
Export data from realms to a file or directory.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help This help message.
|
||||||
|
--help-all This same help message but with additional options.
|
||||||
|
|
||||||
|
Storage (Experimental):
|
||||||
|
|
||||||
|
--storage-deployment-state-version-seed <type>
|
||||||
|
Experimental: Secret that serves as a seed to mask the version number of
|
||||||
|
Keycloak in URLs. Need to be identical across all servers in the cluster.
|
||||||
|
Will default to a random number generated when starting the server which is
|
||||||
|
secure but will lead to problems when a loadbalancer without sticky sessions
|
||||||
|
is used or nodes are restarted.
|
||||||
|
--storage-file-dir <dir>
|
||||||
|
Experimental: Root directory for file map store.
|
||||||
|
--storage-hotrod-host <host>
|
||||||
|
Experimental: Sets the host of the Infinispan server.
|
||||||
|
--storage-hotrod-password <password>
|
||||||
|
Experimental: Sets the password of the Infinispan user.
|
||||||
|
--storage-hotrod-port <port>
|
||||||
|
Experimental: Sets the port of the Infinispan server.
|
||||||
|
--storage-hotrod-username <username>
|
||||||
|
Experimental: Sets the username of the Infinispan user.
|
||||||
|
|
||||||
|
Database:
|
||||||
|
|
||||||
|
--db-password <password>
|
||||||
|
The password of the database user.
|
||||||
|
--db-pool-initial-size <size>
|
||||||
|
The initial size of the connection pool.
|
||||||
|
--db-pool-max-size <size>
|
||||||
|
The maximum size of the connection pool. Default: 100.
|
||||||
|
--db-pool-min-size <size>
|
||||||
|
The minimal size of the connection pool.
|
||||||
|
--db-schema <schema> The database schema to be used.
|
||||||
|
--db-url <jdbc-url> The full database JDBC URL. If not provided, a default URL is set based on the
|
||||||
|
selected database vendor. For instance, if using 'postgres', the default
|
||||||
|
JDBC URL would be 'jdbc:postgresql://localhost/keycloak'.
|
||||||
|
--db-url-database <dbname>
|
||||||
|
Sets the database name of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-url-host <hostname>
|
||||||
|
Sets the hostname of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-url-port <port> Sets the port of the default JDBC URL of the chosen vendor. If the `db-url`
|
||||||
|
option is set, this option is ignored.
|
||||||
|
--db-url-properties <properties>
|
||||||
|
Sets the properties of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-username <username>
|
||||||
|
The username of the database user.
|
||||||
|
|
||||||
|
Vault:
|
||||||
|
|
||||||
|
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
|
||||||
|
given directory.
|
||||||
|
|
||||||
|
Logging:
|
||||||
|
|
||||||
|
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
|
||||||
|
are: console, file, gelf. Default: console.
|
||||||
|
--log-console-color <true|false>
|
||||||
|
Enable or disable colors when logging to console. Default: false.
|
||||||
|
--log-console-format <format>
|
||||||
|
The format of unstructured console log entries. If the format has spaces in
|
||||||
|
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
|
||||||
|
-5p [%c] (%t) %s%e%n.
|
||||||
|
--log-console-output <output>
|
||||||
|
Set the log output to JSON or default (plain) unstructured logging. Possible
|
||||||
|
values are: default, json. Default: default.
|
||||||
|
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
|
||||||
|
--log-file-format <format>
|
||||||
|
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
|
||||||
|
SSS} %-5p [%c] (%t) %s%e%n.
|
||||||
|
--log-file-output <output>
|
||||||
|
Set the log output to JSON or default (plain) unstructured logging. Possible
|
||||||
|
values are: default, json. Default: default.
|
||||||
|
--log-gelf-facility <name>
|
||||||
|
The facility (name of the process) that sends the message. Default: keycloak.
|
||||||
|
--log-gelf-host <hostname>
|
||||||
|
Hostname of the Logstash or Graylog Host. By default UDP is used, prefix the
|
||||||
|
host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' Default:
|
||||||
|
localhost.
|
||||||
|
--log-gelf-include-location <true|false>
|
||||||
|
Include source code location. Default: true.
|
||||||
|
--log-gelf-include-message-parameters <true|false>
|
||||||
|
Include message parameters from the log event. Default: true.
|
||||||
|
--log-gelf-include-stack-trace <true|false>
|
||||||
|
If set to true, occuring stack traces are included in the 'StackTrace' field
|
||||||
|
in the GELF output. Default: true.
|
||||||
|
--log-gelf-level <level>
|
||||||
|
The log level specifying which message levels will be logged by the GELF
|
||||||
|
logger. Message levels lower than this value will be discarded. Default:
|
||||||
|
INFO.
|
||||||
|
--log-gelf-max-message-size <size>
|
||||||
|
Maximum message size (in bytes). If the message size is exceeded, GELF will
|
||||||
|
submit the message in multiple chunks. Default: 8192.
|
||||||
|
--log-gelf-port <port>
|
||||||
|
The port the Logstash or Graylog Host is called on. Default: 12201.
|
||||||
|
--log-gelf-timestamp-format <pattern>
|
||||||
|
Set the format for the GELF timestamp field. Uses Java SimpleDateFormat
|
||||||
|
pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
|
||||||
|
--log-level <category:level>
|
||||||
|
The log level of the root category or a comma-separated list of individual
|
||||||
|
categories and their levels. For the root category, you don't need to
|
||||||
|
specify a category. Default: info.
|
||||||
|
|
||||||
|
Export:
|
||||||
|
|
||||||
|
--dir <dir> Set the path to a directory where files will be created with the exported data.
|
||||||
|
--file <file> Set the path to a file that will be created with the exported data.
|
||||||
|
--realm <realm> Set the name of the realm to export. If not set, all realms are going to be
|
||||||
|
exported.
|
||||||
|
--users <strategy> Set how users should be exported. Possible values are: skip, realm_file,
|
||||||
|
same_file, different_files. Default: different_files.
|
||||||
|
--users-per-file <number>
|
||||||
|
Set the number of users per file. It is used only if 'users' is set to
|
||||||
|
'different_files'. Default: 50.
|
|
@ -0,0 +1,103 @@
|
||||||
|
Import data from a directory or a file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
kc.sh import [OPTIONS]
|
||||||
|
|
||||||
|
Import data from a directory or a file.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help This help message.
|
||||||
|
--help-all This same help message but with additional options.
|
||||||
|
|
||||||
|
Database:
|
||||||
|
|
||||||
|
--db-password <password>
|
||||||
|
The password of the database user.
|
||||||
|
--db-pool-initial-size <size>
|
||||||
|
The initial size of the connection pool.
|
||||||
|
--db-pool-max-size <size>
|
||||||
|
The maximum size of the connection pool. Default: 100.
|
||||||
|
--db-pool-min-size <size>
|
||||||
|
The minimal size of the connection pool.
|
||||||
|
--db-schema <schema> The database schema to be used.
|
||||||
|
--db-url <jdbc-url> The full database JDBC URL. If not provided, a default URL is set based on the
|
||||||
|
selected database vendor. For instance, if using 'postgres', the default
|
||||||
|
JDBC URL would be 'jdbc:postgresql://localhost/keycloak'.
|
||||||
|
--db-url-database <dbname>
|
||||||
|
Sets the database name of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-url-host <hostname>
|
||||||
|
Sets the hostname of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-url-port <port> Sets the port of the default JDBC URL of the chosen vendor. If the `db-url`
|
||||||
|
option is set, this option is ignored.
|
||||||
|
--db-url-properties <properties>
|
||||||
|
Sets the properties of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-username <username>
|
||||||
|
The username of the database user.
|
||||||
|
|
||||||
|
Vault:
|
||||||
|
|
||||||
|
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
|
||||||
|
given directory.
|
||||||
|
|
||||||
|
Logging:
|
||||||
|
|
||||||
|
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
|
||||||
|
are: console, file, gelf. Default: console.
|
||||||
|
--log-console-color <true|false>
|
||||||
|
Enable or disable colors when logging to console. Default: false.
|
||||||
|
--log-console-format <format>
|
||||||
|
The format of unstructured console log entries. If the format has spaces in
|
||||||
|
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
|
||||||
|
-5p [%c] (%t) %s%e%n.
|
||||||
|
--log-console-output <output>
|
||||||
|
Set the log output to JSON or default (plain) unstructured logging. Possible
|
||||||
|
values are: default, json. Default: default.
|
||||||
|
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
|
||||||
|
--log-file-format <format>
|
||||||
|
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
|
||||||
|
SSS} %-5p [%c] (%t) %s%e%n.
|
||||||
|
--log-file-output <output>
|
||||||
|
Set the log output to JSON or default (plain) unstructured logging. Possible
|
||||||
|
values are: default, json. Default: default.
|
||||||
|
--log-gelf-facility <name>
|
||||||
|
The facility (name of the process) that sends the message. Default: keycloak.
|
||||||
|
--log-gelf-host <hostname>
|
||||||
|
Hostname of the Logstash or Graylog Host. By default UDP is used, prefix the
|
||||||
|
host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' Default:
|
||||||
|
localhost.
|
||||||
|
--log-gelf-include-location <true|false>
|
||||||
|
Include source code location. Default: true.
|
||||||
|
--log-gelf-include-message-parameters <true|false>
|
||||||
|
Include message parameters from the log event. Default: true.
|
||||||
|
--log-gelf-include-stack-trace <true|false>
|
||||||
|
If set to true, occuring stack traces are included in the 'StackTrace' field
|
||||||
|
in the GELF output. Default: true.
|
||||||
|
--log-gelf-level <level>
|
||||||
|
The log level specifying which message levels will be logged by the GELF
|
||||||
|
logger. Message levels lower than this value will be discarded. Default:
|
||||||
|
INFO.
|
||||||
|
--log-gelf-max-message-size <size>
|
||||||
|
Maximum message size (in bytes). If the message size is exceeded, GELF will
|
||||||
|
submit the message in multiple chunks. Default: 8192.
|
||||||
|
--log-gelf-port <port>
|
||||||
|
The port the Logstash or Graylog Host is called on. Default: 12201.
|
||||||
|
--log-gelf-timestamp-format <pattern>
|
||||||
|
Set the format for the GELF timestamp field. Uses Java SimpleDateFormat
|
||||||
|
pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
|
||||||
|
--log-level <category:level>
|
||||||
|
The log level of the root category or a comma-separated list of individual
|
||||||
|
categories and their levels. For the root category, you don't need to
|
||||||
|
specify a category. Default: info.
|
||||||
|
|
||||||
|
Import:
|
||||||
|
|
||||||
|
--dir <dir> Set the path to a directory where files will be read from.
|
||||||
|
--file <file> Set the path to a file that will be read.
|
||||||
|
--override <true|false>
|
||||||
|
Set if existing data should be overwritten. If set to false, data will be
|
||||||
|
ignored. Default: true.
|
|
@ -0,0 +1,122 @@
|
||||||
|
Import data from a directory or a file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
kc.sh import [OPTIONS]
|
||||||
|
|
||||||
|
Import data from a directory or a file.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help This help message.
|
||||||
|
--help-all This same help message but with additional options.
|
||||||
|
|
||||||
|
Storage (Experimental):
|
||||||
|
|
||||||
|
--storage-deployment-state-version-seed <type>
|
||||||
|
Experimental: Secret that serves as a seed to mask the version number of
|
||||||
|
Keycloak in URLs. Need to be identical across all servers in the cluster.
|
||||||
|
Will default to a random number generated when starting the server which is
|
||||||
|
secure but will lead to problems when a loadbalancer without sticky sessions
|
||||||
|
is used or nodes are restarted.
|
||||||
|
--storage-file-dir <dir>
|
||||||
|
Experimental: Root directory for file map store.
|
||||||
|
--storage-hotrod-host <host>
|
||||||
|
Experimental: Sets the host of the Infinispan server.
|
||||||
|
--storage-hotrod-password <password>
|
||||||
|
Experimental: Sets the password of the Infinispan user.
|
||||||
|
--storage-hotrod-port <port>
|
||||||
|
Experimental: Sets the port of the Infinispan server.
|
||||||
|
--storage-hotrod-username <username>
|
||||||
|
Experimental: Sets the username of the Infinispan user.
|
||||||
|
|
||||||
|
Database:
|
||||||
|
|
||||||
|
--db-password <password>
|
||||||
|
The password of the database user.
|
||||||
|
--db-pool-initial-size <size>
|
||||||
|
The initial size of the connection pool.
|
||||||
|
--db-pool-max-size <size>
|
||||||
|
The maximum size of the connection pool. Default: 100.
|
||||||
|
--db-pool-min-size <size>
|
||||||
|
The minimal size of the connection pool.
|
||||||
|
--db-schema <schema> The database schema to be used.
|
||||||
|
--db-url <jdbc-url> The full database JDBC URL. If not provided, a default URL is set based on the
|
||||||
|
selected database vendor. For instance, if using 'postgres', the default
|
||||||
|
JDBC URL would be 'jdbc:postgresql://localhost/keycloak'.
|
||||||
|
--db-url-database <dbname>
|
||||||
|
Sets the database name of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-url-host <hostname>
|
||||||
|
Sets the hostname of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-url-port <port> Sets the port of the default JDBC URL of the chosen vendor. If the `db-url`
|
||||||
|
option is set, this option is ignored.
|
||||||
|
--db-url-properties <properties>
|
||||||
|
Sets the properties of the default JDBC URL of the chosen vendor. If the
|
||||||
|
`db-url` option is set, this option is ignored.
|
||||||
|
--db-username <username>
|
||||||
|
The username of the database user.
|
||||||
|
|
||||||
|
Vault:
|
||||||
|
|
||||||
|
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
|
||||||
|
given directory.
|
||||||
|
|
||||||
|
Logging:
|
||||||
|
|
||||||
|
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
|
||||||
|
are: console, file, gelf. Default: console.
|
||||||
|
--log-console-color <true|false>
|
||||||
|
Enable or disable colors when logging to console. Default: false.
|
||||||
|
--log-console-format <format>
|
||||||
|
The format of unstructured console log entries. If the format has spaces in
|
||||||
|
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
|
||||||
|
-5p [%c] (%t) %s%e%n.
|
||||||
|
--log-console-output <output>
|
||||||
|
Set the log output to JSON or default (plain) unstructured logging. Possible
|
||||||
|
values are: default, json. Default: default.
|
||||||
|
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
|
||||||
|
--log-file-format <format>
|
||||||
|
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
|
||||||
|
SSS} %-5p [%c] (%t) %s%e%n.
|
||||||
|
--log-file-output <output>
|
||||||
|
Set the log output to JSON or default (plain) unstructured logging. Possible
|
||||||
|
values are: default, json. Default: default.
|
||||||
|
--log-gelf-facility <name>
|
||||||
|
The facility (name of the process) that sends the message. Default: keycloak.
|
||||||
|
--log-gelf-host <hostname>
|
||||||
|
Hostname of the Logstash or Graylog Host. By default UDP is used, prefix the
|
||||||
|
host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' Default:
|
||||||
|
localhost.
|
||||||
|
--log-gelf-include-location <true|false>
|
||||||
|
Include source code location. Default: true.
|
||||||
|
--log-gelf-include-message-parameters <true|false>
|
||||||
|
Include message parameters from the log event. Default: true.
|
||||||
|
--log-gelf-include-stack-trace <true|false>
|
||||||
|
If set to true, occuring stack traces are included in the 'StackTrace' field
|
||||||
|
in the GELF output. Default: true.
|
||||||
|
--log-gelf-level <level>
|
||||||
|
The log level specifying which message levels will be logged by the GELF
|
||||||
|
logger. Message levels lower than this value will be discarded. Default:
|
||||||
|
INFO.
|
||||||
|
--log-gelf-max-message-size <size>
|
||||||
|
Maximum message size (in bytes). If the message size is exceeded, GELF will
|
||||||
|
submit the message in multiple chunks. Default: 8192.
|
||||||
|
--log-gelf-port <port>
|
||||||
|
The port the Logstash or Graylog Host is called on. Default: 12201.
|
||||||
|
--log-gelf-timestamp-format <pattern>
|
||||||
|
Set the format for the GELF timestamp field. Uses Java SimpleDateFormat
|
||||||
|
pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
|
||||||
|
--log-level <category:level>
|
||||||
|
The log level of the root category or a comma-separated list of individual
|
||||||
|
categories and their levels. For the root category, you don't need to
|
||||||
|
specify a category. Default: info.
|
||||||
|
|
||||||
|
Import:
|
||||||
|
|
||||||
|
--dir <dir> Set the path to a directory where files will be read from.
|
||||||
|
--file <file> Set the path to a file that will be read.
|
||||||
|
--override <true|false>
|
||||||
|
Set if existing data should be overwritten. If set to false, data will be
|
||||||
|
ignored. Default: true.
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package org.keycloak.exportimport;
|
package org.keycloak.exportimport;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -27,13 +26,10 @@ import java.io.IOException;
|
||||||
*/
|
*/
|
||||||
public interface ImportProvider extends Provider {
|
public interface ImportProvider extends Provider {
|
||||||
|
|
||||||
void importModel(KeycloakSessionFactory factory, Strategy strategy) throws IOException;
|
void importModel() throws IOException;
|
||||||
|
|
||||||
void importRealm(KeycloakSessionFactory factory, String realmName, Strategy strategy) throws IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true if master realm was previously exported and is available in the data to be imported
|
* @return true, if master realm was previously exported and is available in the data to be imported
|
||||||
* @throws IOException
|
|
||||||
*/
|
*/
|
||||||
boolean isMasterRealmExported() throws IOException;
|
boolean isMasterRealmExported() throws IOException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,18 +62,10 @@ public class ExportImportConfig {
|
||||||
System.setProperty(ACTION, exportImportAction);
|
System.setProperty(ACTION, exportImportAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getProvider() {
|
|
||||||
return System.getProperty(PROVIDER, PROVIDER_DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setProvider(String exportImportProvider) {
|
public static void setProvider(String exportImportProvider) {
|
||||||
System.setProperty(PROVIDER, exportImportProvider);
|
System.setProperty(PROVIDER, exportImportProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getRealmName() {
|
|
||||||
return System.getProperty(REALM_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setRealmName(String realmName) {
|
public static void setRealmName(String realmName) {
|
||||||
if (realmName != null) {
|
if (realmName != null) {
|
||||||
System.setProperty(REALM_NAME, realmName);
|
System.setProperty(REALM_NAME, realmName);
|
||||||
|
@ -82,27 +74,14 @@ public class ExportImportConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getDir() {
|
public static void setDir(String dir) {
|
||||||
return System.getProperty(DIR);
|
System.setProperty(DIR, dir);
|
||||||
}
|
|
||||||
|
|
||||||
public static String setDir(String dir) {
|
|
||||||
return System.setProperty(DIR, dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getFile() {
|
|
||||||
return System.getProperty(FILE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setFile(String file) {
|
public static void setFile(String file) {
|
||||||
System.setProperty(FILE, file);
|
System.setProperty(FILE, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Strategy getStrategy() {
|
|
||||||
String strategy = System.getProperty(STRATEGY, DEFAULT_STRATEGY.toString());
|
|
||||||
return Enum.valueOf(Strategy.class, strategy);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isReplacePlaceholders() {
|
public static boolean isReplacePlaceholders() {
|
||||||
return Boolean.getBoolean(REPLACE_PLACEHOLDERS);
|
return Boolean.getBoolean(REPLACE_PLACEHOLDERS);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package org.keycloak.exportimport;
|
package org.keycloak.exportimport;
|
||||||
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -25,7 +24,6 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.KeycloakSessionTask;
|
import org.keycloak.models.KeycloakSessionTask;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.services.ServicesLogger;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -33,6 +31,7 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -47,10 +46,8 @@ public class ExportImportManager {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(ExportImportManager.class);
|
private static final Logger logger = Logger.getLogger(ExportImportManager.class);
|
||||||
|
|
||||||
private KeycloakSessionFactory sessionFactory;
|
private final KeycloakSessionFactory sessionFactory;
|
||||||
private KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
|
||||||
private final String realmName;
|
|
||||||
|
|
||||||
private ExportProvider exportProvider;
|
private ExportProvider exportProvider;
|
||||||
private ImportProvider importProvider;
|
private ImportProvider importProvider;
|
||||||
|
@ -59,9 +56,6 @@ public class ExportImportManager {
|
||||||
this.sessionFactory = session.getKeycloakSessionFactory();
|
this.sessionFactory = session.getKeycloakSessionFactory();
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
|
||||||
realmName = ExportImportConfig.getRealmName();
|
|
||||||
|
|
||||||
String providerId = ExportImportConfig.getProvider();
|
|
||||||
String exportImportAction = ExportImportConfig.getAction();
|
String exportImportAction = ExportImportConfig.getAction();
|
||||||
|
|
||||||
if (ExportImportConfig.ACTION_EXPORT.equals(exportImportAction)) {
|
if (ExportImportConfig.ACTION_EXPORT.equals(exportImportAction)) {
|
||||||
|
@ -70,12 +64,13 @@ public class ExportImportManager {
|
||||||
// Setting this to "provider" doesn't work yet when instrumenting Keycloak with Quarkus as it leads to
|
// 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"
|
// "java.lang.NullPointerException: Cannot invoke "String.indexOf(String)" because "value" is null"
|
||||||
// when calling "Config.getProvider()" from "KeycloakProcessor.loadFactories()"
|
// when calling "Config.getProvider()" from "KeycloakProcessor.loadFactories()"
|
||||||
providerId = Config.scope("export").get("exporter", System.getProperty(PROVIDER, PROVIDER_DEFAULT));
|
String providerId = System.getProperty(PROVIDER, Config.scope("export").get("exporter", 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");
|
||||||
}
|
}
|
||||||
} else if (ExportImportConfig.ACTION_IMPORT.equals(exportImportAction)) {
|
} else if (ExportImportConfig.ACTION_IMPORT.equals(exportImportAction)) {
|
||||||
|
String providerId = System.getProperty(PROVIDER, Config.scope("import").get("importer", PROVIDER_DEFAULT));
|
||||||
importProvider = session.getProvider(ImportProvider.class, providerId);
|
importProvider = session.getProvider(ImportProvider.class, providerId);
|
||||||
if (importProvider == null) {
|
if (importProvider == null) {
|
||||||
throw new RuntimeException("Import provider '" + providerId + "' not found");
|
throw new RuntimeException("Import provider '" + providerId + "' not found");
|
||||||
|
@ -104,21 +99,13 @@ public class ExportImportManager {
|
||||||
|
|
||||||
public void runImport() {
|
public void runImport() {
|
||||||
try {
|
try {
|
||||||
Strategy strategy = ExportImportConfig.getStrategy();
|
importProvider.importModel();
|
||||||
if (realmName == null) {
|
|
||||||
ServicesLogger.LOGGER.fullModelImport(strategy.toString());
|
|
||||||
importProvider.importModel(sessionFactory, strategy);
|
|
||||||
} else {
|
|
||||||
ServicesLogger.LOGGER.realmImportRequested(realmName, strategy.toString());
|
|
||||||
importProvider.importRealm(sessionFactory, realmName, strategy);
|
|
||||||
}
|
|
||||||
ServicesLogger.LOGGER.importSuccess();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("Failed to run import", e);
|
throw new RuntimeException("Failed to run import", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runImportAtStartup(String dir, Strategy strategy) throws IOException {
|
public void runImportAtStartup(String dir) throws IOException {
|
||||||
ExportImportConfig.setReplacePlaceholders(true);
|
ExportImportConfig.setReplacePlaceholders(true);
|
||||||
ExportImportConfig.setAction("import");
|
ExportImportConfig.setAction("import");
|
||||||
|
|
||||||
|
@ -130,11 +117,13 @@ public class ExportImportManager {
|
||||||
if ("dir".equals(providerId)) {
|
if ("dir".equals(providerId)) {
|
||||||
ExportImportConfig.setDir(dir);
|
ExportImportConfig.setDir(dir);
|
||||||
ImportProvider importProvider = session.getProvider(ImportProvider.class, providerId);
|
ImportProvider importProvider = session.getProvider(ImportProvider.class, providerId);
|
||||||
importProvider.importModel(sessionFactory, strategy);
|
importProvider.importModel();
|
||||||
} else if ("singleFile".equals(providerId)) {
|
} else if ("singleFile".equals(providerId)) {
|
||||||
Set<String> filesToImport = new HashSet<>();
|
Set<String> filesToImport = new HashSet<>();
|
||||||
|
|
||||||
for (File file : Paths.get(dir).toFile().listFiles()) {
|
File[] files = Paths.get(dir).toFile().listFiles();
|
||||||
|
Objects.requireNonNull(files, "directory not found");
|
||||||
|
for (File file : files) {
|
||||||
Path filePath = file.toPath();
|
Path filePath = file.toPath();
|
||||||
|
|
||||||
if (!(Files.exists(filePath) && Files.isRegularFile(filePath) && filePath.toString().endsWith(".json"))) {
|
if (!(Files.exists(filePath) && Files.isRegularFile(filePath) && filePath.toString().endsWith(".json"))) {
|
||||||
|
@ -158,7 +147,7 @@ public class ExportImportManager {
|
||||||
public void run(KeycloakSession session) {
|
public void run(KeycloakSession session) {
|
||||||
ImportProvider importProvider = session.getProvider(ImportProvider.class, providerId);
|
ImportProvider importProvider = session.getProvider(ImportProvider.class, providerId);
|
||||||
try {
|
try {
|
||||||
importProvider.importModel(sessionFactory, strategy);
|
importProvider.importModel();
|
||||||
} catch (IOException cause) {
|
} catch (IOException cause) {
|
||||||
throw new RuntimeException(cause);
|
throw new RuntimeException(cause);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.crypto.CryptoIntegration;
|
import org.keycloak.common.crypto.CryptoIntegration;
|
||||||
import org.keycloak.common.util.Resteasy;
|
import org.keycloak.common.util.Resteasy;
|
||||||
import org.keycloak.config.ConfigProviderFactory;
|
import org.keycloak.config.ConfigProviderFactory;
|
||||||
|
import org.keycloak.exportimport.ExportImportConfig;
|
||||||
import org.keycloak.exportimport.ExportImportManager;
|
import org.keycloak.exportimport.ExportImportManager;
|
||||||
import org.keycloak.exportimport.Strategy;
|
import org.keycloak.exportimport.Strategy;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -256,7 +257,8 @@ public class KeycloakApplication extends Application {
|
||||||
String dir = System.getProperty("keycloak.import");
|
String dir = System.getProperty("keycloak.import");
|
||||||
if (dir != null) {
|
if (dir != null) {
|
||||||
try {
|
try {
|
||||||
exportImportManager.runImportAtStartup(dir, Strategy.IGNORE_EXISTING);
|
System.setProperty(ExportImportConfig.STRATEGY, Strategy.IGNORE_EXISTING.toString());
|
||||||
|
exportImportManager.runImportAtStartup(dir);
|
||||||
} catch (IOException cause) {
|
} catch (IOException cause) {
|
||||||
throw new RuntimeException("Failed to import realms", cause);
|
throw new RuntimeException("Failed to import realms", cause);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,6 @@ import org.keycloak.common.profile.PropertiesProfileConfigResolver;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.component.ComponentFactoryProviderFactory;
|
import org.keycloak.component.ComponentFactoryProviderFactory;
|
||||||
import org.keycloak.component.ComponentFactorySpi;
|
import org.keycloak.component.ComponentFactorySpi;
|
||||||
import org.keycloak.device.DeviceRepresentationProviderFactoryImpl;
|
|
||||||
import org.keycloak.device.DeviceRepresentationSpi;
|
|
||||||
import org.keycloak.events.EventStoreSpi;
|
import org.keycloak.events.EventStoreSpi;
|
||||||
import org.keycloak.executors.DefaultExecutorsProviderFactory;
|
import org.keycloak.executors.DefaultExecutorsProviderFactory;
|
||||||
import org.keycloak.executors.ExecutorsSpi;
|
import org.keycloak.executors.ExecutorsSpi;
|
||||||
|
@ -279,9 +277,9 @@ public abstract class KeycloakModelTest {
|
||||||
Stream.of(basicParameters),
|
Stream.of(basicParameters),
|
||||||
Stream.of(System.getProperty("keycloak.model.parameters", "").split("\\s*,\\s*"))
|
Stream.of(System.getProperty("keycloak.model.parameters", "").split("\\s*,\\s*"))
|
||||||
.filter(s -> s != null && ! s.trim().isEmpty())
|
.filter(s -> s != null && ! s.trim().isEmpty())
|
||||||
.map(cn -> { try { return Class.forName(cn.indexOf('.') >= 0 ? cn : ("org.keycloak.testsuite.model.parameters." + cn)); } catch (Exception e) { LOG.error("Cannot find " + cn); return null; }})
|
.map(cn -> { try { return Class.forName(cn.indexOf('.') >= 0 ? cn : ("org.keycloak.testsuite.model.parameters." + cn)); } catch (Exception e) { throw new RuntimeException("Cannot find class " + cn, e); }})
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.map(c -> { try { return c.getDeclaredConstructor().newInstance(); } catch (Exception e) { LOG.error("Cannot instantiate " + c); return null; }} )
|
.map(c -> { try { return c.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("Cannot instantiate class " + c, e); }} )
|
||||||
.filter(KeycloakModelParameters.class::isInstance)
|
.filter(KeycloakModelParameters.class::isInstance)
|
||||||
.map(KeycloakModelParameters.class::cast)
|
.map(KeycloakModelParameters.class::cast)
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.model.export;
|
package org.keycloak.testsuite.model.exportimport;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
@ -76,11 +76,10 @@ public class ExportModelTest extends KeycloakModelTest {
|
||||||
.provider(SingleFileExportProviderFactory.PROVIDER_ID)
|
.provider(SingleFileExportProviderFactory.PROVIDER_ID)
|
||||||
.config(SingleFileExportProviderFactory.REALM_NAME, REALM_NAME);
|
.config(SingleFileExportProviderFactory.REALM_NAME, REALM_NAME);
|
||||||
|
|
||||||
withRealm(realmId, (session, realm) -> {
|
inComittedTransaction(session -> {
|
||||||
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
|
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
|
||||||
ExportImportManager exportImportManager = new ExportImportManager(session);
|
ExportImportManager exportImportManager = new ExportImportManager(session);
|
||||||
exportImportManager.runExport();
|
exportImportManager.runExport();
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// file will exist if export was successful
|
// file will exist if export was successful
|
||||||
|
@ -112,11 +111,10 @@ public class ExportModelTest extends KeycloakModelTest {
|
||||||
.provider(DirExportProviderFactory.PROVIDER_ID)
|
.provider(DirExportProviderFactory.PROVIDER_ID)
|
||||||
.config(DirExportProviderFactory.REALM_NAME, REALM_NAME);
|
.config(DirExportProviderFactory.REALM_NAME, REALM_NAME);
|
||||||
|
|
||||||
withRealm(realmId, (session, realm) -> {
|
inComittedTransaction(session -> {
|
||||||
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
|
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
|
||||||
ExportImportManager exportImportManager = new ExportImportManager(session);
|
ExportImportManager exportImportManager = new ExportImportManager(session);
|
||||||
exportImportManager.runExport();
|
exportImportManager.runExport();
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// file will exist if export was successful
|
// file will exist if export was successful
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.model.exportimport;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.exportimport.ExportImportConfig;
|
||||||
|
import org.keycloak.exportimport.ExportImportManager;
|
||||||
|
import org.keycloak.exportimport.ExportProvider;
|
||||||
|
import org.keycloak.exportimport.ImportProvider;
|
||||||
|
import org.keycloak.exportimport.dir.DirExportProviderFactory;
|
||||||
|
import org.keycloak.exportimport.dir.DirImportProviderFactory;
|
||||||
|
import org.keycloak.exportimport.singlefile.SingleFileImportProviderFactory;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||||
|
import org.keycloak.testsuite.model.KeycloakModelTest;
|
||||||
|
import org.keycloak.testsuite.model.RequireProvider;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@RequireProvider(value = ImportProvider.class)
|
||||||
|
public class ImportModelTest extends KeycloakModelTest {
|
||||||
|
|
||||||
|
public static final String SPI_NAME = "import";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createEnvironment(KeycloakSession s) {
|
||||||
|
// Master realm is needed for importing a realm
|
||||||
|
if (s.realms().getRealmByName("master") == null) {
|
||||||
|
new ApplianceBootstrap(s).createMasterRealm();
|
||||||
|
}
|
||||||
|
// clean-up test realm which might be left-over from a previous run
|
||||||
|
RealmModel test = s.realms().getRealmByName("test");
|
||||||
|
if (test != null) {
|
||||||
|
s.realms().removeRealm(test.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanEnvironment(KeycloakSession s) {
|
||||||
|
RealmModel master = s.realms().getRealmByName("master");
|
||||||
|
if (master != null) {
|
||||||
|
s.realms().removeRealm(master.getId());
|
||||||
|
}
|
||||||
|
RealmModel test = s.realms().getRealmByName("test");
|
||||||
|
if (test != null) {
|
||||||
|
s.realms().removeRealm(test.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequireProvider(value = ExportProvider.class, only = SingleFileImportProviderFactory.PROVIDER_ID)
|
||||||
|
public void testImportSingleFile() {
|
||||||
|
try {
|
||||||
|
Path singleFileExport = Paths.get("src/test/resources/exportimport/singleFile/testrealm.json");
|
||||||
|
|
||||||
|
CONFIG.spi(SPI_NAME)
|
||||||
|
.config("importer", new SingleFileImportProviderFactory().getId());
|
||||||
|
CONFIG.spi(SPI_NAME)
|
||||||
|
.provider(SingleFileImportProviderFactory.PROVIDER_ID)
|
||||||
|
.config(SingleFileImportProviderFactory.FILE, singleFileExport.toAbsolutePath().toString());
|
||||||
|
|
||||||
|
inComittedTransaction(session -> {
|
||||||
|
ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT);
|
||||||
|
ExportImportManager exportImportManager = new ExportImportManager(session);
|
||||||
|
exportImportManager.runImport();
|
||||||
|
});
|
||||||
|
|
||||||
|
inComittedTransaction(session -> {
|
||||||
|
Assert.assertNotNull(session.realms().getRealmByName("test"));
|
||||||
|
});
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
CONFIG.spi(SPI_NAME)
|
||||||
|
.config("importer", null);
|
||||||
|
CONFIG.spi(SPI_NAME)
|
||||||
|
.provider(SingleFileImportProviderFactory.PROVIDER_ID)
|
||||||
|
.config(SingleFileImportProviderFactory.FILE, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequireProvider(value = ExportProvider.class, only = DirImportProviderFactory.PROVIDER_ID)
|
||||||
|
public void testImportDirectory() {
|
||||||
|
try {
|
||||||
|
Path importFolder = Paths.get("src/test/resources/exportimport/dir");
|
||||||
|
CONFIG.spi(SPI_NAME)
|
||||||
|
.config("importer", new DirImportProviderFactory().getId());
|
||||||
|
CONFIG.spi(SPI_NAME)
|
||||||
|
.provider(DirImportProviderFactory.PROVIDER_ID)
|
||||||
|
.config(DirImportProviderFactory.DIR, importFolder.toAbsolutePath().toString());
|
||||||
|
|
||||||
|
inComittedTransaction(session -> {
|
||||||
|
ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT);
|
||||||
|
ExportImportManager exportImportManager = new ExportImportManager(session);
|
||||||
|
exportImportManager.runImport();
|
||||||
|
});
|
||||||
|
|
||||||
|
inComittedTransaction(session -> {
|
||||||
|
Assert.assertNotNull(session.realms().getRealmByName("test"));
|
||||||
|
});
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
CONFIG.spi(SPI_NAME)
|
||||||
|
.config("importer", null);
|
||||||
|
CONFIG.spi(SPI_NAME)
|
||||||
|
.provider(DirImportProviderFactory.PROVIDER_ID)
|
||||||
|
.config(DirExportProviderFactory.DIR, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,12 +16,22 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.model.parameters;
|
package org.keycloak.testsuite.model.parameters;
|
||||||
|
|
||||||
|
import org.keycloak.common.crypto.CryptoIntegration;
|
||||||
|
import org.keycloak.common.crypto.CryptoProvider;
|
||||||
import org.keycloak.exportimport.ExportSpi;
|
import org.keycloak.exportimport.ExportSpi;
|
||||||
|
import org.keycloak.exportimport.ImportSpi;
|
||||||
import org.keycloak.exportimport.dir.DirExportProviderFactory;
|
import org.keycloak.exportimport.dir.DirExportProviderFactory;
|
||||||
|
import org.keycloak.exportimport.dir.DirImportProviderFactory;
|
||||||
import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
|
import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
|
||||||
|
import org.keycloak.exportimport.singlefile.SingleFileImportProviderFactory;
|
||||||
|
import org.keycloak.keys.KeyProviderFactory;
|
||||||
|
import org.keycloak.keys.KeySpi;
|
||||||
|
import org.keycloak.models.ClientScopeSpi;
|
||||||
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.ClientPolicyManagerFactory;
|
||||||
import org.keycloak.services.clientpolicy.ClientPolicyManagerSpi;
|
import org.keycloak.services.clientpolicy.ClientPolicyManagerSpi;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi;
|
||||||
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;
|
||||||
|
@ -39,13 +49,30 @@ 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(ExportSpi.class)
|
||||||
.add(ClientPolicyManagerSpi.class)
|
.add(ClientPolicyManagerSpi.class)
|
||||||
|
.add(ImportSpi.class)
|
||||||
|
.add(ClientRegistrationPolicySpi.class)
|
||||||
|
.add(ClientScopeSpi.class)
|
||||||
|
.add(KeySpi.class)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
static {
|
||||||
|
// CryptoIntegration needed for import of realms
|
||||||
|
CryptoIntegration.init(CryptoProvider.class.getClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
// start providers needed for export
|
||||||
.add(SingleFileExportProviderFactory.class)
|
.add(SingleFileExportProviderFactory.class)
|
||||||
.add(DirExportProviderFactory.class)
|
.add(DirExportProviderFactory.class)
|
||||||
.add(ClientPolicyManagerFactory.class)
|
.add(ClientPolicyManagerFactory.class)
|
||||||
|
// end providers needed for export
|
||||||
|
// start providers needed for import
|
||||||
|
.add(SingleFileImportProviderFactory.class)
|
||||||
|
.add(DirImportProviderFactory.class)
|
||||||
|
.add(ClientRegistrationPolicyFactory.class)
|
||||||
|
.add(KeyProviderFactory.class)
|
||||||
|
// end providers needed for import
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -18,9 +18,6 @@ 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;
|
||||||
|
@ -37,8 +34,6 @@ 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;
|
||||||
|
|
|
@ -0,0 +1,682 @@
|
||||||
|
{
|
||||||
|
"id": "4b390336-f32b-4b49-b0a5-e7c865637496",
|
||||||
|
"realm": "test",
|
||||||
|
"enabled": true,
|
||||||
|
"sslRequired": "external",
|
||||||
|
"registrationAllowed": true,
|
||||||
|
"resetPasswordAllowed": true,
|
||||||
|
"editUsernameAllowed" : true,
|
||||||
|
"ssoSessionIdleTimeout": 1800,
|
||||||
|
"ssoSessionMaxLifespan": 36000,
|
||||||
|
"offlineSessionIdleTimeout": 2592000,
|
||||||
|
"offlineSessionMaxLifespan": 5184000,
|
||||||
|
"requiredCredentials": [ "password" ],
|
||||||
|
"defaultRoles": [ "user" ],
|
||||||
|
"smtpServer": {
|
||||||
|
"from": "auto@keycloak.org",
|
||||||
|
"host": "localhost",
|
||||||
|
"port":"3025",
|
||||||
|
"fromDisplayName": "Keycloak SSO",
|
||||||
|
"replyTo":"reply-to@keycloak.org",
|
||||||
|
"replyToDisplayName": "Keycloak no-reply",
|
||||||
|
"envelopeFrom": "auto+bounces@keycloak.org"
|
||||||
|
},
|
||||||
|
"users" : [
|
||||||
|
{
|
||||||
|
"username" : "test-user@localhost",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "test-user@localhost",
|
||||||
|
"firstName": "Tom",
|
||||||
|
"lastName": "Brady",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"realmRoles": ["user", "offline_access"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": [ "customer-user" ],
|
||||||
|
"account": [ "view-profile", "manage-account" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "john-doh@localhost",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "john-doh@localhost",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doh",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"realmRoles": ["user"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": [ "customer-user" ],
|
||||||
|
"account": [ "view-profile", "manage-account" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "keycloak-user@localhost",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "keycloak-user@localhost",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"realmRoles": ["user"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": [ "customer-user" ],
|
||||||
|
"account": [ "view-profile", "manage-account" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "topGroupUser",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "top@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
"/topGroup"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "level2GroupUser",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "level2@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
"/topGroup/level2group"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "roleRichUser",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "rich.roles@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
"/roleRichGroup/level2group"
|
||||||
|
],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "non-duplicate-email-user",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "non-duplicate-email-user@localhost",
|
||||||
|
"firstName": "Brian",
|
||||||
|
"lastName": "Cohen",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"realmRoles": ["user", "offline_access"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": [ "customer-user" ],
|
||||||
|
"account": [ "view-profile", "manage-account" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "user-with-one-configured-otp",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "otp1@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{
|
||||||
|
"type" : "password",
|
||||||
|
"value" : "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "unique",
|
||||||
|
"type" : "otp",
|
||||||
|
"secretData" : "{\"value\":\"DJmQfC73VGFhw7D4QJ8A\"}",
|
||||||
|
"credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "user-with-two-configured-otp",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "otp2@redhat.com",
|
||||||
|
"realmRoles": ["user"],
|
||||||
|
"credentials" : [
|
||||||
|
{
|
||||||
|
"id" : "first",
|
||||||
|
"userLabel" : "first",
|
||||||
|
"type" : "otp",
|
||||||
|
"secretData" : "{\"value\":\"DJmQfC73VGFhw7D4QJ8A\"}",
|
||||||
|
"credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type" : "password",
|
||||||
|
"value" : "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "second",
|
||||||
|
"type" : "otp",
|
||||||
|
"secretData" : "{\"value\":\"ABCQfC73VGFhw7D4QJ8A\"}",
|
||||||
|
"credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scopeMappings": [
|
||||||
|
{
|
||||||
|
"client": "third-party",
|
||||||
|
"roles": ["user"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "test-app",
|
||||||
|
"roles": ["user"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "test-app-scope",
|
||||||
|
"roles": ["user", "admin"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"clientId": "test-app",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "http://localhost:8180/auth/realms/master/app/auth",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/auth/realms/master/app/auth/*",
|
||||||
|
"https://localhost:8543/auth/realms/master/app/auth/*",
|
||||||
|
"http://localhost:8180/auth/realms/test/app/auth/*",
|
||||||
|
"https://localhost:8543/auth/realms/test/app/auth/*"
|
||||||
|
],
|
||||||
|
"adminUrl": "http://localhost:8180/auth/realms/master/app/admin",
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "root-url-client",
|
||||||
|
"enabled": true,
|
||||||
|
"rootUrl": "http://localhost:8180/foo/bar",
|
||||||
|
"adminUrl": "http://localhost:8180/foo/bar",
|
||||||
|
"baseUrl": "/baz",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/foo/bar/*",
|
||||||
|
"https://localhost:8543/foo/bar/*"
|
||||||
|
],
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId" : "test-app-scope",
|
||||||
|
"enabled": true,
|
||||||
|
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/auth/realms/master/app/*",
|
||||||
|
"https://localhost:8543/auth/realms/master/app/*"
|
||||||
|
],
|
||||||
|
"secret": "password",
|
||||||
|
"fullScopeAllowed": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId" : "third-party",
|
||||||
|
"description" : "A third party application",
|
||||||
|
"enabled": true,
|
||||||
|
"consentRequired": true,
|
||||||
|
|
||||||
|
"baseUrl": "http://localhost:8180/auth/realms/master/app/auth",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/auth/realms/master/app/*",
|
||||||
|
"https://localhost:8543/auth/realms/master/app/*"
|
||||||
|
],
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "test-app-authz",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "/test-app-authz",
|
||||||
|
"adminUrl": "/test-app-authz",
|
||||||
|
"bearerOnly": false,
|
||||||
|
"authorizationSettings": {
|
||||||
|
"allowRemoteResourceManagement": true,
|
||||||
|
"policyEnforcementMode": "ENFORCING",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "Admin Resource",
|
||||||
|
"uri": "/protected/admin/*",
|
||||||
|
"type": "http://test-app-authz/protected/admin",
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "admin-access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Protected Resource",
|
||||||
|
"uri": "/*",
|
||||||
|
"type": "http://test-app-authz/protected/resource",
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "resource-access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Premium Resource",
|
||||||
|
"uri": "/protected/premium/*",
|
||||||
|
"type": "urn:test-app-authz:protected:resource",
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "premium-access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Main Page",
|
||||||
|
"type": "urn:test-app-authz:protected:resource",
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "urn:test-app-authz:page:main:actionForAdmin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "urn:test-app-authz:page:main:actionForUser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "urn:test-app-authz:page:main:actionForPremiumUser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"name": "Any Admin Policy",
|
||||||
|
"description": "Defines that adminsitrators can do something",
|
||||||
|
"type": "role",
|
||||||
|
"config": {
|
||||||
|
"roles": "[{\"id\":\"admin\"}]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Any User Policy",
|
||||||
|
"description": "Defines that any user can do something",
|
||||||
|
"type": "role",
|
||||||
|
"config": {
|
||||||
|
"roles": "[{\"id\":\"user\"}]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Only Premium User Policy",
|
||||||
|
"description": "Defines that only premium users can do something",
|
||||||
|
"type": "role",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"config": {
|
||||||
|
"roles": "[{\"id\":\"customer-user-premium\"}]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All Users Policy",
|
||||||
|
"description": "Defines that all users can do something",
|
||||||
|
"type": "aggregate",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"applyPolicies": "[\"Any User Policy\",\"Any Admin Policy\",\"Only Premium User Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Premium Resource Permission",
|
||||||
|
"description": "A policy that defines access to premium resources",
|
||||||
|
"type": "resource",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"Premium Resource\"]",
|
||||||
|
"applyPolicies": "[\"Only Premium User Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Administrative Resource Permission",
|
||||||
|
"description": "A policy that defines access to administrative resources",
|
||||||
|
"type": "resource",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"Admin Resource\"]",
|
||||||
|
"applyPolicies": "[\"Any Admin Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Protected Resource Permission",
|
||||||
|
"description": "A policy that defines access to any protected resource",
|
||||||
|
"type": "resource",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"Protected Resource\"]",
|
||||||
|
"applyPolicies": "[\"All Users Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Action 1 on Main Page Resource Permission",
|
||||||
|
"description": "A policy that defines access to action 1 on the main page",
|
||||||
|
"type": "scope",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"scopes": "[\"urn:test-app-authz:page:main:actionForAdmin\"]",
|
||||||
|
"applyPolicies": "[\"Any Admin Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Action 2 on Main Page Resource Permission",
|
||||||
|
"description": "A policy that defines access to action 2 on the main page",
|
||||||
|
"type": "scope",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"scopes": "[\"urn:test-app-authz:page:main:actionForUser\"]",
|
||||||
|
"applyPolicies": "[\"Any User Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Action 3 on Main Page Resource Permission",
|
||||||
|
"description": "A policy that defines access to action 3 on the main page",
|
||||||
|
"type": "scope",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"scopes": "[\"urn:test-app-authz:page:main:actionForPremiumUser\"]",
|
||||||
|
"applyPolicies": "[\"Only Premium User Policy\"]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"redirectUris": [
|
||||||
|
"/test-app-authz/*"
|
||||||
|
],
|
||||||
|
"secret": "secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "named-test-app",
|
||||||
|
"name": "My Named Test App",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "http://localhost:8180/namedapp/base",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/namedapp/base/*",
|
||||||
|
"https://localhost:8543/namedapp/base/*"
|
||||||
|
],
|
||||||
|
"adminUrl": "http://localhost:8180/namedapp/base/admin",
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "var-named-test-app",
|
||||||
|
"name": "Test App Named - ${client_account}",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "http://localhost:8180/varnamedapp/base",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/varnamedapp/base/*",
|
||||||
|
"https://localhost:8543/varnamedapp/base/*"
|
||||||
|
],
|
||||||
|
"adminUrl": "http://localhost:8180/varnamedapp/base/admin",
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "direct-grant",
|
||||||
|
"enabled": true,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"secret": "password",
|
||||||
|
"webOrigins": [ "http://localtest.me:8180" ],
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "aud-account",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-audience-mapper",
|
||||||
|
"config": {
|
||||||
|
"included.client.audience": "account",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aud-admin",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-audience-mapper",
|
||||||
|
"config": {
|
||||||
|
"included.client.audience": "security-admin-console",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"clientId": "custom-audience",
|
||||||
|
"enabled": true,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"secret": "password",
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "aud",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-audience-mapper",
|
||||||
|
"config": {
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"included.custom.audience": "foo-bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "client roles",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-client-role-mapper",
|
||||||
|
"config": {
|
||||||
|
"user.attribute": "foo",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "resource_access.${client_id}.roles",
|
||||||
|
"jsonType.label": "String",
|
||||||
|
"multivalued": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "realm roles",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-realm-role-mapper",
|
||||||
|
"config": {
|
||||||
|
"user.attribute": "foo",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "realm_access.roles",
|
||||||
|
"jsonType.label": "String",
|
||||||
|
"multivalued": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultClientScopes": [
|
||||||
|
"web-origins",
|
||||||
|
"profile",
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles" : {
|
||||||
|
"realm" : [
|
||||||
|
{
|
||||||
|
"name": "user",
|
||||||
|
"description": "Have User privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"description": "Have Administrator privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-user-premium",
|
||||||
|
"description": "Have User Premium privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sample-realm-role",
|
||||||
|
"description": "Sample realm role"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "attribute-role",
|
||||||
|
"description": "has attributes assigned",
|
||||||
|
"attributes": {
|
||||||
|
"hello": [
|
||||||
|
"world",
|
||||||
|
"keycloak"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "realm-composite-role",
|
||||||
|
"description": "Realm composite role containing client role",
|
||||||
|
"composite" : true,
|
||||||
|
"composites" : {
|
||||||
|
"realm" : [ "sample-realm-role" ],
|
||||||
|
"client" : {
|
||||||
|
"test-app" : [ "sample-client-role" ],
|
||||||
|
"account" : [ "view-profile" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"client" : {
|
||||||
|
"test-app" : [
|
||||||
|
{
|
||||||
|
"name": "manage-account",
|
||||||
|
"description": "Allows application-initiated actions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-user",
|
||||||
|
"description": "Have Customer User privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-admin",
|
||||||
|
"description": "Have Customer Admin privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sample-client-role",
|
||||||
|
"description": "Sample client role",
|
||||||
|
"attributes": {
|
||||||
|
"sample-client-role-attribute": [
|
||||||
|
"sample-client-role-attribute-value"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-admin-composite-role",
|
||||||
|
"description": "Have Customer Admin privileges via composite role",
|
||||||
|
"composite" : true,
|
||||||
|
"composites" : {
|
||||||
|
"realm" : [ "customer-user-premium" ],
|
||||||
|
"client" : {
|
||||||
|
"test-app" : [ "customer-admin" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"test-app-scope" : [
|
||||||
|
{
|
||||||
|
"name": "test-app-allowed-by-scope",
|
||||||
|
"description": "Role allowed by scope in test-app-scope"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test-app-disallowed-by-scope",
|
||||||
|
"description": "Role disallowed by scope in test-app-scope"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"name": "topGroup",
|
||||||
|
"attributes": {
|
||||||
|
"topAttribute": ["true"]
|
||||||
|
|
||||||
|
},
|
||||||
|
"realmRoles": ["user"],
|
||||||
|
|
||||||
|
"subGroups": [
|
||||||
|
{
|
||||||
|
"name": "level2group",
|
||||||
|
"realmRoles": ["admin"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": ["customer-user"]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"level2Attribute": ["true"]
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "level2group2",
|
||||||
|
"realmRoles": ["admin"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": ["customer-user"]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"level2Attribute": ["true"]
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "roleRichGroup",
|
||||||
|
"attributes": {
|
||||||
|
"topAttribute": ["true"]
|
||||||
|
|
||||||
|
},
|
||||||
|
"realmRoles": ["user", "realm-composite-role"],
|
||||||
|
"clientRoles": {
|
||||||
|
"account": ["manage-account"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"subGroups": [
|
||||||
|
{
|
||||||
|
"name": "level2group",
|
||||||
|
"realmRoles": ["admin"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": ["customer-user", "customer-admin-composite-role"]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"level2Attribute": ["true"]
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "level2group2",
|
||||||
|
"realmRoles": ["admin"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": ["customer-user"]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"level2Attribute": ["true"]
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sample-realm-group"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
"clientScopeMappings": {
|
||||||
|
"test-app": [
|
||||||
|
{
|
||||||
|
"client": "third-party",
|
||||||
|
"roles": ["customer-user"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "test-app-scope",
|
||||||
|
"roles": ["customer-admin-composite-role"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"test-app-scope": [
|
||||||
|
{
|
||||||
|
"client": "test-app-scope",
|
||||||
|
"roles": ["test-app-allowed-by-scope"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"internationalizationEnabled": true,
|
||||||
|
"supportedLocales": ["en", "de"],
|
||||||
|
"defaultLocale": "en",
|
||||||
|
"eventsListeners": ["jboss-logging", "event-queue"]
|
||||||
|
}
|
|
@ -0,0 +1,682 @@
|
||||||
|
{
|
||||||
|
"id": "4b390336-f32b-4b49-b0a5-e7c865637496",
|
||||||
|
"realm": "test",
|
||||||
|
"enabled": true,
|
||||||
|
"sslRequired": "external",
|
||||||
|
"registrationAllowed": true,
|
||||||
|
"resetPasswordAllowed": true,
|
||||||
|
"editUsernameAllowed" : true,
|
||||||
|
"ssoSessionIdleTimeout": 1800,
|
||||||
|
"ssoSessionMaxLifespan": 36000,
|
||||||
|
"offlineSessionIdleTimeout": 2592000,
|
||||||
|
"offlineSessionMaxLifespan": 5184000,
|
||||||
|
"requiredCredentials": [ "password" ],
|
||||||
|
"defaultRoles": [ "user" ],
|
||||||
|
"smtpServer": {
|
||||||
|
"from": "auto@keycloak.org",
|
||||||
|
"host": "localhost",
|
||||||
|
"port":"3025",
|
||||||
|
"fromDisplayName": "Keycloak SSO",
|
||||||
|
"replyTo":"reply-to@keycloak.org",
|
||||||
|
"replyToDisplayName": "Keycloak no-reply",
|
||||||
|
"envelopeFrom": "auto+bounces@keycloak.org"
|
||||||
|
},
|
||||||
|
"users" : [
|
||||||
|
{
|
||||||
|
"username" : "test-user@localhost",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "test-user@localhost",
|
||||||
|
"firstName": "Tom",
|
||||||
|
"lastName": "Brady",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"realmRoles": ["user", "offline_access"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": [ "customer-user" ],
|
||||||
|
"account": [ "view-profile", "manage-account" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "john-doh@localhost",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "john-doh@localhost",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doh",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"realmRoles": ["user"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": [ "customer-user" ],
|
||||||
|
"account": [ "view-profile", "manage-account" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "keycloak-user@localhost",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "keycloak-user@localhost",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"realmRoles": ["user"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": [ "customer-user" ],
|
||||||
|
"account": [ "view-profile", "manage-account" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "topGroupUser",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "top@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
"/topGroup"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "level2GroupUser",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "level2@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
"/topGroup/level2group"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "roleRichUser",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "rich.roles@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
"/roleRichGroup/level2group"
|
||||||
|
],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "non-duplicate-email-user",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "non-duplicate-email-user@localhost",
|
||||||
|
"firstName": "Brian",
|
||||||
|
"lastName": "Cohen",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"realmRoles": ["user", "offline_access"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": [ "customer-user" ],
|
||||||
|
"account": [ "view-profile", "manage-account" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "user-with-one-configured-otp",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "otp1@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{
|
||||||
|
"type" : "password",
|
||||||
|
"value" : "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "unique",
|
||||||
|
"type" : "otp",
|
||||||
|
"secretData" : "{\"value\":\"DJmQfC73VGFhw7D4QJ8A\"}",
|
||||||
|
"credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username" : "user-with-two-configured-otp",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "otp2@redhat.com",
|
||||||
|
"realmRoles": ["user"],
|
||||||
|
"credentials" : [
|
||||||
|
{
|
||||||
|
"id" : "first",
|
||||||
|
"userLabel" : "first",
|
||||||
|
"type" : "otp",
|
||||||
|
"secretData" : "{\"value\":\"DJmQfC73VGFhw7D4QJ8A\"}",
|
||||||
|
"credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type" : "password",
|
||||||
|
"value" : "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "second",
|
||||||
|
"type" : "otp",
|
||||||
|
"secretData" : "{\"value\":\"ABCQfC73VGFhw7D4QJ8A\"}",
|
||||||
|
"credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scopeMappings": [
|
||||||
|
{
|
||||||
|
"client": "third-party",
|
||||||
|
"roles": ["user"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "test-app",
|
||||||
|
"roles": ["user"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "test-app-scope",
|
||||||
|
"roles": ["user", "admin"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"clientId": "test-app",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "http://localhost:8180/auth/realms/master/app/auth",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/auth/realms/master/app/auth/*",
|
||||||
|
"https://localhost:8543/auth/realms/master/app/auth/*",
|
||||||
|
"http://localhost:8180/auth/realms/test/app/auth/*",
|
||||||
|
"https://localhost:8543/auth/realms/test/app/auth/*"
|
||||||
|
],
|
||||||
|
"adminUrl": "http://localhost:8180/auth/realms/master/app/admin",
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "root-url-client",
|
||||||
|
"enabled": true,
|
||||||
|
"rootUrl": "http://localhost:8180/foo/bar",
|
||||||
|
"adminUrl": "http://localhost:8180/foo/bar",
|
||||||
|
"baseUrl": "/baz",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/foo/bar/*",
|
||||||
|
"https://localhost:8543/foo/bar/*"
|
||||||
|
],
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId" : "test-app-scope",
|
||||||
|
"enabled": true,
|
||||||
|
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/auth/realms/master/app/*",
|
||||||
|
"https://localhost:8543/auth/realms/master/app/*"
|
||||||
|
],
|
||||||
|
"secret": "password",
|
||||||
|
"fullScopeAllowed": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId" : "third-party",
|
||||||
|
"description" : "A third party application",
|
||||||
|
"enabled": true,
|
||||||
|
"consentRequired": true,
|
||||||
|
|
||||||
|
"baseUrl": "http://localhost:8180/auth/realms/master/app/auth",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/auth/realms/master/app/*",
|
||||||
|
"https://localhost:8543/auth/realms/master/app/*"
|
||||||
|
],
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "test-app-authz",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "/test-app-authz",
|
||||||
|
"adminUrl": "/test-app-authz",
|
||||||
|
"bearerOnly": false,
|
||||||
|
"authorizationSettings": {
|
||||||
|
"allowRemoteResourceManagement": true,
|
||||||
|
"policyEnforcementMode": "ENFORCING",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "Admin Resource",
|
||||||
|
"uri": "/protected/admin/*",
|
||||||
|
"type": "http://test-app-authz/protected/admin",
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "admin-access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Protected Resource",
|
||||||
|
"uri": "/*",
|
||||||
|
"type": "http://test-app-authz/protected/resource",
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "resource-access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Premium Resource",
|
||||||
|
"uri": "/protected/premium/*",
|
||||||
|
"type": "urn:test-app-authz:protected:resource",
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "premium-access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Main Page",
|
||||||
|
"type": "urn:test-app-authz:protected:resource",
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "urn:test-app-authz:page:main:actionForAdmin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "urn:test-app-authz:page:main:actionForUser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "urn:test-app-authz:page:main:actionForPremiumUser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"name": "Any Admin Policy",
|
||||||
|
"description": "Defines that adminsitrators can do something",
|
||||||
|
"type": "role",
|
||||||
|
"config": {
|
||||||
|
"roles": "[{\"id\":\"admin\"}]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Any User Policy",
|
||||||
|
"description": "Defines that any user can do something",
|
||||||
|
"type": "role",
|
||||||
|
"config": {
|
||||||
|
"roles": "[{\"id\":\"user\"}]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Only Premium User Policy",
|
||||||
|
"description": "Defines that only premium users can do something",
|
||||||
|
"type": "role",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"config": {
|
||||||
|
"roles": "[{\"id\":\"customer-user-premium\"}]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All Users Policy",
|
||||||
|
"description": "Defines that all users can do something",
|
||||||
|
"type": "aggregate",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"applyPolicies": "[\"Any User Policy\",\"Any Admin Policy\",\"Only Premium User Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Premium Resource Permission",
|
||||||
|
"description": "A policy that defines access to premium resources",
|
||||||
|
"type": "resource",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"Premium Resource\"]",
|
||||||
|
"applyPolicies": "[\"Only Premium User Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Administrative Resource Permission",
|
||||||
|
"description": "A policy that defines access to administrative resources",
|
||||||
|
"type": "resource",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"Admin Resource\"]",
|
||||||
|
"applyPolicies": "[\"Any Admin Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Protected Resource Permission",
|
||||||
|
"description": "A policy that defines access to any protected resource",
|
||||||
|
"type": "resource",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"Protected Resource\"]",
|
||||||
|
"applyPolicies": "[\"All Users Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Action 1 on Main Page Resource Permission",
|
||||||
|
"description": "A policy that defines access to action 1 on the main page",
|
||||||
|
"type": "scope",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"scopes": "[\"urn:test-app-authz:page:main:actionForAdmin\"]",
|
||||||
|
"applyPolicies": "[\"Any Admin Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Action 2 on Main Page Resource Permission",
|
||||||
|
"description": "A policy that defines access to action 2 on the main page",
|
||||||
|
"type": "scope",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"scopes": "[\"urn:test-app-authz:page:main:actionForUser\"]",
|
||||||
|
"applyPolicies": "[\"Any User Policy\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Action 3 on Main Page Resource Permission",
|
||||||
|
"description": "A policy that defines access to action 3 on the main page",
|
||||||
|
"type": "scope",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"scopes": "[\"urn:test-app-authz:page:main:actionForPremiumUser\"]",
|
||||||
|
"applyPolicies": "[\"Only Premium User Policy\"]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"redirectUris": [
|
||||||
|
"/test-app-authz/*"
|
||||||
|
],
|
||||||
|
"secret": "secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "named-test-app",
|
||||||
|
"name": "My Named Test App",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "http://localhost:8180/namedapp/base",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/namedapp/base/*",
|
||||||
|
"https://localhost:8543/namedapp/base/*"
|
||||||
|
],
|
||||||
|
"adminUrl": "http://localhost:8180/namedapp/base/admin",
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "var-named-test-app",
|
||||||
|
"name": "Test App Named - ${client_account}",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "http://localhost:8180/varnamedapp/base",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8180/varnamedapp/base/*",
|
||||||
|
"https://localhost:8543/varnamedapp/base/*"
|
||||||
|
],
|
||||||
|
"adminUrl": "http://localhost:8180/varnamedapp/base/admin",
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "direct-grant",
|
||||||
|
"enabled": true,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"secret": "password",
|
||||||
|
"webOrigins": [ "http://localtest.me:8180" ],
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "aud-account",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-audience-mapper",
|
||||||
|
"config": {
|
||||||
|
"included.client.audience": "account",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aud-admin",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-audience-mapper",
|
||||||
|
"config": {
|
||||||
|
"included.client.audience": "security-admin-console",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"clientId": "custom-audience",
|
||||||
|
"enabled": true,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"secret": "password",
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "aud",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-audience-mapper",
|
||||||
|
"config": {
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"included.custom.audience": "foo-bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "client roles",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-client-role-mapper",
|
||||||
|
"config": {
|
||||||
|
"user.attribute": "foo",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "resource_access.${client_id}.roles",
|
||||||
|
"jsonType.label": "String",
|
||||||
|
"multivalued": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "realm roles",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-realm-role-mapper",
|
||||||
|
"config": {
|
||||||
|
"user.attribute": "foo",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "realm_access.roles",
|
||||||
|
"jsonType.label": "String",
|
||||||
|
"multivalued": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultClientScopes": [
|
||||||
|
"web-origins",
|
||||||
|
"profile",
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles" : {
|
||||||
|
"realm" : [
|
||||||
|
{
|
||||||
|
"name": "user",
|
||||||
|
"description": "Have User privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"description": "Have Administrator privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-user-premium",
|
||||||
|
"description": "Have User Premium privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sample-realm-role",
|
||||||
|
"description": "Sample realm role"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "attribute-role",
|
||||||
|
"description": "has attributes assigned",
|
||||||
|
"attributes": {
|
||||||
|
"hello": [
|
||||||
|
"world",
|
||||||
|
"keycloak"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "realm-composite-role",
|
||||||
|
"description": "Realm composite role containing client role",
|
||||||
|
"composite" : true,
|
||||||
|
"composites" : {
|
||||||
|
"realm" : [ "sample-realm-role" ],
|
||||||
|
"client" : {
|
||||||
|
"test-app" : [ "sample-client-role" ],
|
||||||
|
"account" : [ "view-profile" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"client" : {
|
||||||
|
"test-app" : [
|
||||||
|
{
|
||||||
|
"name": "manage-account",
|
||||||
|
"description": "Allows application-initiated actions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-user",
|
||||||
|
"description": "Have Customer User privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-admin",
|
||||||
|
"description": "Have Customer Admin privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sample-client-role",
|
||||||
|
"description": "Sample client role",
|
||||||
|
"attributes": {
|
||||||
|
"sample-client-role-attribute": [
|
||||||
|
"sample-client-role-attribute-value"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-admin-composite-role",
|
||||||
|
"description": "Have Customer Admin privileges via composite role",
|
||||||
|
"composite" : true,
|
||||||
|
"composites" : {
|
||||||
|
"realm" : [ "customer-user-premium" ],
|
||||||
|
"client" : {
|
||||||
|
"test-app" : [ "customer-admin" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"test-app-scope" : [
|
||||||
|
{
|
||||||
|
"name": "test-app-allowed-by-scope",
|
||||||
|
"description": "Role allowed by scope in test-app-scope"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test-app-disallowed-by-scope",
|
||||||
|
"description": "Role disallowed by scope in test-app-scope"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"name": "topGroup",
|
||||||
|
"attributes": {
|
||||||
|
"topAttribute": ["true"]
|
||||||
|
|
||||||
|
},
|
||||||
|
"realmRoles": ["user"],
|
||||||
|
|
||||||
|
"subGroups": [
|
||||||
|
{
|
||||||
|
"name": "level2group",
|
||||||
|
"realmRoles": ["admin"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": ["customer-user"]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"level2Attribute": ["true"]
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "level2group2",
|
||||||
|
"realmRoles": ["admin"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": ["customer-user"]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"level2Attribute": ["true"]
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "roleRichGroup",
|
||||||
|
"attributes": {
|
||||||
|
"topAttribute": ["true"]
|
||||||
|
|
||||||
|
},
|
||||||
|
"realmRoles": ["user", "realm-composite-role"],
|
||||||
|
"clientRoles": {
|
||||||
|
"account": ["manage-account"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"subGroups": [
|
||||||
|
{
|
||||||
|
"name": "level2group",
|
||||||
|
"realmRoles": ["admin"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": ["customer-user", "customer-admin-composite-role"]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"level2Attribute": ["true"]
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "level2group2",
|
||||||
|
"realmRoles": ["admin"],
|
||||||
|
"clientRoles": {
|
||||||
|
"test-app": ["customer-user"]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"level2Attribute": ["true"]
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sample-realm-group"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
"clientScopeMappings": {
|
||||||
|
"test-app": [
|
||||||
|
{
|
||||||
|
"client": "third-party",
|
||||||
|
"roles": ["customer-user"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "test-app-scope",
|
||||||
|
"roles": ["customer-admin-composite-role"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"test-app-scope": [
|
||||||
|
{
|
||||||
|
"client": "test-app-scope",
|
||||||
|
"roles": ["test-app-allowed-by-scope"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"internationalizationEnabled": true,
|
||||||
|
"supportedLocales": ["en", "de"],
|
||||||
|
"defaultLocale": "en",
|
||||||
|
"eventsListeners": ["jboss-logging", "event-queue"]
|
||||||
|
}
|
Loading…
Reference in a new issue