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:
Alexander Schwartz 2023-03-15 12:57:49 +01:00 committed by Pedro Igor
parent d29f3e4dfc
commit 251f6151e8
36 changed files with 2478 additions and 220 deletions

View file

@ -28,14 +28,15 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.platform.Platform;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.ServicesLogger;
import org.keycloak.util.JsonSerialization;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import org.keycloak.services.managers.RealmManager;
@ -44,54 +45,73 @@ import org.keycloak.services.managers.RealmManager;
*/
public class DirImportProvider extends AbstractFileBasedImportProvider {
private final Strategy strategy;
private final KeycloakSessionFactory factory;
private static final Logger logger = Logger.getLogger(DirImportProvider.class);
private final File rootDirectory;
private File rootDirectory;
public DirImportProvider() {
// 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");
}
private String realmName;
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) {
this.rootDirectory = rootDirectory;
public DirImportProvider withDir(String dir) {
this.rootDirectory = new File(dir);
if (!this.rootDirectory.exists()) {
throw new IllegalStateException("Directory " + this.rootDirectory + " doesn't exist");
}
logger.infof("Importing from directory %s", this.rootDirectory.getAbsolutePath());
return this;
}
@Override
public void importModel(KeycloakSessionFactory factory, Strategy strategy) throws IOException {
List<String> realmNames = getRealmsToImport();
public DirImportProvider withRealmName(String realmName) {
this.realmName = realmName;
return this;
}
for (String realmName : realmNames) {
importRealm(factory, realmName, strategy);
private File getRootDirectory() {
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
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();
return realmNames.contains(Config.getAdminRealm());
}
private List<String> getRealmsToImport() throws IOException {
File[] realmFiles = this.rootDirectory.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return (name.endsWith("-realm.json"));
}
});
private List<String> getRealmsToImport() {
File[] realmFiles = getRootDirectory().listFiles((dir, name) -> (name.endsWith("-realm.json")));
Objects.requireNonNull(realmFiles, "Directory not found: " + getRootDirectory().getName());
List<String> realmNames = new ArrayList<>();
for (File file : realmFiles) {
String fileName = file.getName();
@ -108,23 +128,12 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
return realmNames;
}
@Override
public void importRealm(KeycloakSessionFactory factory, final String realmName, final Strategy strategy) throws IOException {
File realmFile = new File(this.rootDirectory + File.separator + realmName + "-realm.json");
File[] userFiles = this.rootDirectory.listFiles(new FilenameFilter() {
@Override
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");
}
});
public void importRealm(final String realmName, final Strategy strategy) throws IOException {
File realmFile = new File(getRootDirectory() + File.separator + realmName + "-realm.json");
File[] userFiles = getRootDirectory().listFiles((dir, name) -> name.matches(realmName + "-users-[0-9]+\\.json"));
Objects.requireNonNull(userFiles, "directory not found: " + getRootDirectory().getName());
File[] federatedUserFiles = getRootDirectory().listFiles((dir, name) -> name.matches(realmName + "-federated-users-[0-9]+\\.json"));
Objects.requireNonNull(federatedUserFiles, "directory not found: " + getRootDirectory().getName());
// Import realm first
InputStream is = parseFile(realmFile);
@ -134,7 +143,7 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
public void runExportImportTask(KeycloakSession session) throws IOException {
public void runExportImportTask(KeycloakSession session) {
boolean imported = ImportUtils.importRealm(session, realmRep, strategy, true);
realmImported.set(imported);
}
@ -144,24 +153,26 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
if (realmImported.get()) {
// Import users
for (final File userFile : userFiles) {
final InputStream fis = parseFile(userFile);
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
ImportUtils.importUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
logger.infof("Imported users from %s", userFile.getAbsolutePath());
}
});
try (InputStream fis = parseFile(userFile)) {
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
ImportUtils.importUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
logger.infof("Imported users from %s", userFile.getAbsolutePath());
}
});
}
}
for (final File userFile : federatedUserFiles) {
final InputStream fis = parseFile(userFile);
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
ImportUtils.importFederatedUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
logger.infof("Imported federated users from %s", userFile.getAbsolutePath());
}
});
try (InputStream fis = parseFile(userFile)) {
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
ImportUtils.importFederatedUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
logger.infof("Imported federated users from %s", userFile.getAbsolutePath());
}
});
}
}
}
@ -170,7 +181,7 @@ public class DirImportProvider extends AbstractFileBasedImportProvider {
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
public void runExportImportTask(KeycloakSession session) throws IOException {
public void runExportImportTask(KeycloakSession session) {
RealmManager realmManager = new RealmManager(session);
realmManager.setupClientServiceAccountsAndAuthorizationOnImport(realmRep, false);
}

View file

@ -21,24 +21,42 @@ import org.keycloak.Config;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.ImportProvider;
import org.keycloak.exportimport.ImportProviderFactory;
import org.keycloak.exportimport.Strategy;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.io.File;
import java.util.List;
import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_STRATEGY;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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
public ImportProvider create(KeycloakSession session) {
String dir = ExportImportConfig.getDir();
return dir!=null ? new DirImportProvider(new File(dir)) : new DirImportProvider();
Strategy strategy = Enum.valueOf(Strategy.class, System.getProperty(ExportImportConfig.STRATEGY, config.get(STRATEGY, DEFAULT_STRATEGY.toString())));
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
public void init(Config.Scope config) {
this.config = config;
}
@Override
@ -52,6 +70,30 @@ public class DirImportProviderFactory implements ImportProviderFactory {
@Override
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();
}
}

View file

@ -40,25 +40,28 @@ import java.util.Map;
public class SingleFileImportProvider extends AbstractFileBasedImportProvider {
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
protected Map<String, RealmRepresentation> realmReps;
public SingleFileImportProvider(File file) {
public SingleFileImportProvider(KeycloakSessionFactory factory, File file, Strategy strategy) {
this.factory = factory;
this.file = file;
this.strategy = strategy;
}
@Override
public void importModel(KeycloakSessionFactory factory, final Strategy strategy) throws IOException {
public void importModel() throws IOException {
logger.infof("Full importing from file %s", this.file.getAbsolutePath());
checkRealmReps();
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
protected void runExportImportTask(KeycloakSession session) {
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
public void close() {

View file

@ -21,27 +21,44 @@ import org.keycloak.Config;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.ImportProvider;
import org.keycloak.exportimport.ImportProviderFactory;
import org.keycloak.exportimport.Strategy;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.io.File;
import java.util.List;
import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_STRATEGY;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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
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) {
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
public void init(Config.Scope config) {
this.config = config;
}
@Override
@ -55,6 +72,30 @@ public class SingleFileImportProviderFactory implements ImportProviderFactory {
@Override
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();
}
}

View file

@ -21,13 +21,13 @@ public class ExportOptions {
public static final Option<String> FILE = new OptionBuilder<>("file", String.class)
.category(OptionCategory.EXPORT)
.hidden() // hidden for now until we refactor the import command
.description("Set the path to a file that will be created with the exported data.")
.buildTime(false)
.build();
public static final Option<String> DIR = new OptionBuilder<>("dir", String.class)
.category(OptionCategory.EXPORT)
.hidden() // hidden for now until we refactor the import command
.description("Set the path to a directory where files will be created with the exported data.")
.buildTime(false)
.build();

View file

@ -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();
}

View file

@ -16,9 +16,10 @@ public enum OptionCategory {
LOGGING("Logging", 110, ConfigSupportLevel.SUPPORTED),
SECURITY("Security", 120, ConfigSupportLevel.PREVIEW),
EXPORT("Export", 130, ConfigSupportLevel.SUPPORTED),
IMPORT("Import", 140, 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
private final int order;
private final ConfigSupportLevel supportLevel;

View file

@ -50,6 +50,7 @@ import java.util.stream.Collectors;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.keycloak.config.MultiOption;
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.Build;
import org.keycloak.quarkus.runtime.cli.command.Export;
@ -347,7 +348,7 @@ public final class Picocli {
if (isRebuildCheck()) {
// 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);
@ -361,16 +362,22 @@ public final class Picocli {
return cmd;
}
private static void addCommandOptions(List<String> cliArgs, CommandSpec command) {
private static void addCommandOptions(List<String> cliArgs, CommandLine command) {
if (command != null) {
boolean includeBuildTime = 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);
includeRuntime = true;
} else if (Build.NAME.equals(command.name())) {
includeBuildTime = true;
} else if (includeBuildTime && !includeRuntime) {
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) {
CommandLine command = spec.subcommands().get(arg);
if (command != null) {
return command.getCommandSpec();
return command;
}
}
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);
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) {
for(OptionCategory category : OptionCategory.values()) {
if (cSpec.name().equals(Export.NAME)) {
// The export command should show only export options
if (category != OptionCategory.EXPORT) {
continue;
}
} else {
// No other command should have the export options
if (category == OptionCategory.EXPORT) {
continue;
}
}
private static void addMappedOptionsToArgGroups(CommandLine commandLine, Map<OptionCategory, List<PropertyMapper>> propertyMappers) {
CommandSpec cSpec = commandLine.getCommandSpec();
for(OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) {
List<PropertyMapper> mappersInCategory = propertyMappers.get(category);
if (mappersInCategory == null) {

View file

@ -19,10 +19,14 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.quarkus.runtime.Messages.cliExecutionError;
import org.keycloak.config.OptionCategory;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Spec;
import java.util.Arrays;
import java.util.List;
public abstract class AbstractCommand {
@Spec
@ -35,4 +39,25 @@ public abstract class AbstractCommand {
protected void executionError(CommandLine cmd, String message, Throwable 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());
}
}

View file

@ -17,28 +17,19 @@
package org.keycloak.quarkus.runtime.cli.command;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT;
import java.util.List;
import java.util.stream.Collectors;
public abstract class AbstractExportImportCommand extends AbstractStartCommand implements Runnable {
private final String action;
@Option(names = "--dir",
arity = "1",
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;
@CommandLine.Mixin
HelpAllMixin helpAllMixin;
protected AbstractExportImportCommand(String action) {
this.action = action;
@ -48,25 +39,23 @@ public abstract class AbstractExportImportCommand extends AbstractStartCommand i
public void run() {
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);
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;
}
}

View file

@ -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.configuration.ConfigArgsConfigSource.getAllCliArgs;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.Messages;
@ -32,6 +33,9 @@ import io.quarkus.runtime.configuration.ProfileManager;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import java.util.List;
import java.util.stream.Collectors;
@Command(name = Build.NAME,
header = "Creates a new and optimized server image.",
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() {
if (Environment.isDevProfile() && !getAllCliArgs().contains(StartDev.NAME)) {
executionError(spec.commandLine(), Messages.devProfileNotAllowedError(NAME));

View file

@ -19,8 +19,11 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
import org.keycloak.config.OptionCategory;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import java.util.List;
import java.util.stream.Collectors;
@Command(name = Export.NAME,
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);
}
@Override
public List<OptionCategory> getOptionCategories() {
return super.getOptionCategories().stream().filter(optionCategory ->
optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
}
}

View file

@ -18,11 +18,12 @@
package org.keycloak.quarkus.runtime.cli.command;
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.Option;
import java.util.List;
import java.util.stream.Collectors;
@Command(name = Import.NAME,
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";
@Option(names = "--override",
arity = "1",
description = "Set if existing data should be skipped or overridden.",
paramLabel = "false",
defaultValue = "true")
boolean override;
public Import() {
super(ACTION_IMPORT);
}
@Override
protected void doBeforeRun() {
System.setProperty("keycloak.migration.strategy", override ? OVERWRITE_EXISTING.name() : IGNORE_EXISTING.name());
public List<OptionCategory> getOptionCategories() {
return super.getOptionCategories().stream().filter(optionCategory ->
optionCategory != OptionCategory.EXPORT).collect(Collectors.toList());
}
}

View file

@ -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.configuration.Configuration.getRawPersistedProperty;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.Messages;
@ -29,6 +30,7 @@ import picocli.CommandLine.Command;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Command(name = Start.NAME,
header = "Start the server.",
@ -85,4 +87,13 @@ public final class Start extends AbstractStartCommand implements Runnable {
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;
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.quarkus.runtime.cli.command;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import picocli.CommandLine;
@ -24,6 +25,9 @@ import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;
import java.util.List;
import java.util.stream.Collectors;
@Command(name = StartDev.NAME,
header = "Start the server in development mode.",
description = {
@ -48,4 +52,14 @@ public final class StartDev extends AbstractStartCommand implements Runnable {
protected void doBeforeRun() {
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;
}
}

View file

@ -20,9 +20,11 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import org.keycloak.config.ExportOptions;
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 ExportPropertyMappers {
@ -65,6 +67,10 @@ final class ExportPropertyMappers {
}
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()) {
return Optional.of("singleFile");
}
@ -72,6 +78,13 @@ final class ExportPropertyMappers {
if (dirConfigValue != null && dirConfigValue.getValue() != null) {
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();
}

View file

@ -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();
}
}

View file

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

View file

@ -38,6 +38,6 @@ public class ExportDistTest {
cliResult.assertNoMessage("Listening on:");
cliResult = dist.run("export", "--realm=master");
cliResult.assertError("Must specify either --dir or --file options.");
cliResult.assertMessage("Must specify either --dir or --file options.");
}
}

View file

@ -28,6 +28,8 @@ import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.utils.KeycloakDistribution;
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.StartDev;
@ -110,6 +112,34 @@ public class HelpCommandDistTest {
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
public void testHelpDoesNotStartReAugJvm(KeycloakDistribution dist) {
for (String helpCmd : List.of("-h", "--help", "--help-all")) {

View file

@ -0,0 +1 @@
HelpCommandDistTest.*.received.txt

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -17,7 +17,6 @@
package org.keycloak.exportimport;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.Provider;
import java.io.IOException;
@ -27,13 +26,10 @@ import java.io.IOException;
*/
public interface ImportProvider extends Provider {
void importModel(KeycloakSessionFactory factory, Strategy strategy) throws IOException;
void importRealm(KeycloakSessionFactory factory, String realmName, Strategy strategy) throws IOException;
void importModel() throws IOException;
/**
* @return true if master realm was previously exported and is available in the data to be imported
* @throws IOException
* @return true, if master realm was previously exported and is available in the data to be imported
*/
boolean isMasterRealmExported() throws IOException;
}

View file

@ -62,18 +62,10 @@ public class ExportImportConfig {
System.setProperty(ACTION, exportImportAction);
}
public static String getProvider() {
return System.getProperty(PROVIDER, PROVIDER_DEFAULT);
}
public static void setProvider(String exportImportProvider) {
System.setProperty(PROVIDER, exportImportProvider);
}
public static String getRealmName() {
return System.getProperty(REALM_NAME);
}
public static void setRealmName(String realmName) {
if (realmName != null) {
System.setProperty(REALM_NAME, realmName);
@ -82,27 +74,14 @@ public class ExportImportConfig {
}
}
public static String getDir() {
return System.getProperty(DIR);
}
public static String setDir(String dir) {
return System.setProperty(DIR, dir);
}
public static String getFile() {
return System.getProperty(FILE);
public static void setDir(String dir) {
System.setProperty(DIR, dir);
}
public static void setFile(String 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() {
return Boolean.getBoolean(REPLACE_PLACEHOLDERS);
}

View file

@ -17,7 +17,6 @@
package org.keycloak.exportimport;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
@ -25,7 +24,6 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.services.ServicesLogger;
import java.io.File;
import java.io.IOException;
@ -33,6 +31,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -47,10 +46,8 @@ public class ExportImportManager {
private static final Logger logger = Logger.getLogger(ExportImportManager.class);
private KeycloakSessionFactory sessionFactory;
private KeycloakSession session;
private final String realmName;
private final KeycloakSessionFactory sessionFactory;
private final KeycloakSession session;
private ExportProvider exportProvider;
private ImportProvider importProvider;
@ -59,9 +56,6 @@ public class ExportImportManager {
this.sessionFactory = session.getKeycloakSessionFactory();
this.session = session;
realmName = ExportImportConfig.getRealmName();
String providerId = ExportImportConfig.getProvider();
String exportImportAction = ExportImportConfig.getAction();
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
// "java.lang.NullPointerException: Cannot invoke "String.indexOf(String)" because "value" is null"
// when calling "Config.getProvider()" from "KeycloakProcessor.loadFactories()"
providerId = Config.scope("export").get("exporter", System.getProperty(PROVIDER, PROVIDER_DEFAULT));
String providerId = System.getProperty(PROVIDER, Config.scope("export").get("exporter", PROVIDER_DEFAULT));
exportProvider = session.getProvider(ExportProvider.class, providerId);
if (exportProvider == null) {
throw new RuntimeException("Export provider '" + providerId + "' not found");
}
} 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);
if (importProvider == null) {
throw new RuntimeException("Import provider '" + providerId + "' not found");
@ -104,21 +99,13 @@ public class ExportImportManager {
public void runImport() {
try {
Strategy strategy = ExportImportConfig.getStrategy();
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();
importProvider.importModel();
} catch (IOException 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.setAction("import");
@ -130,11 +117,13 @@ public class ExportImportManager {
if ("dir".equals(providerId)) {
ExportImportConfig.setDir(dir);
ImportProvider importProvider = session.getProvider(ImportProvider.class, providerId);
importProvider.importModel(sessionFactory, strategy);
importProvider.importModel();
} else if ("singleFile".equals(providerId)) {
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();
if (!(Files.exists(filePath) && Files.isRegularFile(filePath) && filePath.toString().endsWith(".json"))) {
@ -158,7 +147,7 @@ public class ExportImportManager {
public void run(KeycloakSession session) {
ImportProvider importProvider = session.getProvider(ImportProvider.class, providerId);
try {
importProvider.importModel(sessionFactory, strategy);
importProvider.importModel();
} catch (IOException cause) {
throw new RuntimeException(cause);
}

View file

@ -23,6 +23,7 @@ import org.keycloak.common.Profile;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.Resteasy;
import org.keycloak.config.ConfigProviderFactory;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.ExportImportManager;
import org.keycloak.exportimport.Strategy;
import org.keycloak.models.KeycloakSession;
@ -256,7 +257,8 @@ public class KeycloakApplication extends Application {
String dir = System.getProperty("keycloak.import");
if (dir != null) {
try {
exportImportManager.runImportAtStartup(dir, Strategy.IGNORE_EXISTING);
System.setProperty(ExportImportConfig.STRATEGY, Strategy.IGNORE_EXISTING.toString());
exportImportManager.runImportAtStartup(dir);
} catch (IOException cause) {
throw new RuntimeException("Failed to import realms", cause);
}

View file

@ -30,8 +30,6 @@ import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentFactoryProviderFactory;
import org.keycloak.component.ComponentFactorySpi;
import org.keycloak.device.DeviceRepresentationProviderFactoryImpl;
import org.keycloak.device.DeviceRepresentationSpi;
import org.keycloak.events.EventStoreSpi;
import org.keycloak.executors.DefaultExecutorsProviderFactory;
import org.keycloak.executors.ExecutorsSpi;
@ -279,9 +277,9 @@ public abstract class KeycloakModelTest {
Stream.of(basicParameters),
Stream.of(System.getProperty("keycloak.model.parameters", "").split("\\s*,\\s*"))
.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)
.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)
.map(KeycloakModelParameters.class::cast)
)

View file

@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.testsuite.model.export;
package org.keycloak.testsuite.model.exportimport;
import org.apache.commons.io.FileUtils;
import org.junit.Assert;
@ -76,11 +76,10 @@ public class ExportModelTest extends KeycloakModelTest {
.provider(SingleFileExportProviderFactory.PROVIDER_ID)
.config(SingleFileExportProviderFactory.REALM_NAME, REALM_NAME);
withRealm(realmId, (session, realm) -> {
inComittedTransaction(session -> {
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
ExportImportManager exportImportManager = new ExportImportManager(session);
exportImportManager.runExport();
return null;
});
// file will exist if export was successful
@ -112,11 +111,10 @@ public class ExportModelTest extends KeycloakModelTest {
.provider(DirExportProviderFactory.PROVIDER_ID)
.config(DirExportProviderFactory.REALM_NAME, REALM_NAME);
withRealm(realmId, (session, realm) -> {
inComittedTransaction(session -> {
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
ExportImportManager exportImportManager = new ExportImportManager(session);
exportImportManager.runExport();
return null;
});
// file will exist if export was successful

View file

@ -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);
}
}
}

View file

@ -16,12 +16,22 @@
*/
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.ImportSpi;
import org.keycloak.exportimport.dir.DirExportProviderFactory;
import org.keycloak.exportimport.dir.DirImportProviderFactory;
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.services.clientpolicy.ClientPolicyManagerFactory;
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.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
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()
.add(ExportSpi.class)
.add(ClientPolicyManagerSpi.class)
.add(ImportSpi.class)
.add(ClientRegistrationPolicySpi.class)
.add(ClientScopeSpi.class)
.add(KeySpi.class)
.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()
.add(ConcurrentHashMapStorageProviderFactory.class)
// start providers needed for export
.add(SingleFileExportProviderFactory.class)
.add(DirExportProviderFactory.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();
@Override

View file

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

View file

@ -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"]
}

View file

@ -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"]
}