From 74b3a56cc52b39e8c6c784a193874e372cbb89ea Mon Sep 17 00:00:00 2001 From: Marc Scheib Date: Sun, 14 Jan 2024 22:52:47 +0100 Subject: [PATCH 1/2] feat: adapt importers to new configuration dir structure (#26) --- .../configure/boundary/AbstractImporter.java | 56 ++++-------- .../configure/boundary/ClientImporter.java | 14 +-- .../boundary/ClientRoleImporter.java | 9 +- .../configure/boundary/GroupImporter.java | 14 +-- .../configure/boundary/RealmImporter.java | 3 +- .../configure/boundary/RealmRoleImporter.java | 11 +-- .../configure/boundary/UserImporter.java | 17 ++-- .../control/ConfigurationFileStore.java | 89 +++++++++++++++++++ .../configurator/shared/control/JsonUtil.java | 40 +++++++++ .../shared/control/KeycloakFactory.java | 7 +- src/main/resources/application.properties | 4 +- 11 files changed, 193 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/ConfigurationFileStore.java diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/AbstractImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/AbstractImporter.java index 75bcbf9..5a71cb1 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/AbstractImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/AbstractImporter.java @@ -1,38 +1,36 @@ package com.cycrilabs.keycloak.configurator.commands.configure.boundary; -import static io.quarkus.arc.ComponentsProvider.LOG; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; import java.util.List; import java.util.regex.Pattern; -import java.util.stream.Stream; import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; import org.keycloak.admin.client.Keycloak; +import com.cycrilabs.keycloak.configurator.commands.configure.control.ConfigurationFileStore; import com.cycrilabs.keycloak.configurator.commands.configure.control.EntityStore; import com.cycrilabs.keycloak.configurator.commands.configure.entity.ConfigureCommandConfiguration; -import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; import com.cycrilabs.keycloak.configurator.shared.control.KeycloakFactory; import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; import io.quarkus.logging.Log; public abstract class AbstractImporter { + /** + * The path separator for the current operating system. This is used to split the file path into + * its parts. This is used in a regular expression, so special characters need to be escaped. + */ public static final String PATH_SEPARATOR = Pattern.quote(FileSystems.getDefault().getSeparator()); @Inject protected ConfigureCommandConfiguration configuration; protected Keycloak keycloak; + @Inject + protected ConfigurationFileStore configurationFileStore; protected EntityStore entityStore; @PostConstruct @@ -40,50 +38,28 @@ public void init() { keycloak = KeycloakFactory.create(configuration); } - protected T loadEntity(final Path filepath, final Class dtoClass) { - final String json = loadJsonFromResource(filepath); - return JsonUtil.fromJson(json, dtoClass); - } - - private String loadJsonFromResource(final Path filePath) { - try { - return Files.readString(filePath, StandardCharsets.UTF_8); - } catch (final IOException e) { - LOG.errorf("Could not read file {}", filePath); - throw new RuntimeException(e); - } - } - public void runImport(final EntityStore entityStore) { - Log.infof("Executing importer %s.", getClass().getSimpleName()); + Log.infof("Executing importer '%s'.", getClass().getSimpleName()); this.entityStore = entityStore; - final List importFiles = getEntityFilePaths(getEntityDirectory()); - for (final Path importFile : importFiles) { + + for (final Path importFile : getImportFiles()) { + Log.debugf("Importing file '%s'.", importFile); importFile(importFile); } } - private List getEntityFilePaths(final String entityDir) { - final String dir = Paths.get(configuration.getConfigDirectory(), entityDir).toString(); - try (final Stream stream = Files.walk(Paths.get(dir))) { - return stream - .filter(Files::isRegularFile) - .filter(p -> p.toString().endsWith(".json")) - .toList(); - } catch (final IOException e) { - Log.errorf("Could not read directory %s", dir); - return Collections.emptyList(); + private List getImportFiles() { + final List importFiles = configurationFileStore.getImportFiles(getType()); + if (importFiles.isEmpty()) { + Log.infof("No files found for importer '%s'.", getClass().getSimpleName()); } + return importFiles; } public int getPriority() { return getType().getPriority(); } - protected String getEntityDirectory() { - return getType().getDirectory(); - } - public abstract EntityType getType(); protected abstract Object importFile(final Path file); diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientImporter.java index f811001..b874e9b 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientImporter.java @@ -9,6 +9,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; +import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; import io.quarkus.logging.Log; @@ -22,30 +23,31 @@ public EntityType getType() { @Override protected ClientRepresentation importFile(final Path file) { - final ClientRepresentation client = loadEntity(file, ClientRepresentation.class); + final ClientRepresentation client = JsonUtil.loadEntity(file, ClientRepresentation.class); final String[] fileNameParts = file.toString().split(PATH_SEPARATOR); - final String realmName = fileNameParts[fileNameParts.length - 2]; + final String realmName = fileNameParts[fileNameParts.length - 3]; try (final Response response = keycloak.realm(realmName) .clients() .create(client)) { if (response.getStatus() == 409) { - Log.errorf("Could not import client from file: %s", + Log.errorf("Could not import client for realm '%s': %s", realmName, response.readEntity(ErrorRepresentation.class) .getErrorMessage()); } else { - Log.infof("Client '%s' imported.", client.getClientId()); + Log.infof("Client '%s' imported for realm '%s'.", client.getClientId(), realmName); } } catch (final ClientErrorException e) { - Log.errorf("Could not import client from file: %s", e.getMessage()); + Log.errorf("Could not import client for realm '%s': %s", realmName, e.getMessage()); } final ClientRepresentation importedClient = keycloak.realm(realmName) .clients() .findByClientId(client.getClientId()) .get(0); - Log.infof("Loaded client role '%s' from server.", importedClient.getClientId()); + Log.infof("Loaded client '%s' from realm '%s'.", importedClient.getClientId(), + realmName); entityStore.addClient(realmName, importedClient); return importedClient; } diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientRoleImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientRoleImporter.java index 5130eaa..37f8e19 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientRoleImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientRoleImporter.java @@ -8,6 +8,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RoleRepresentation; +import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; import io.quarkus.logging.Log; @@ -21,7 +22,7 @@ public EntityType getType() { @Override protected RoleRepresentation importFile(final Path file) { - final RoleRepresentation role = loadEntity(file, RoleRepresentation.class); + final RoleRepresentation role = JsonUtil.loadEntity(file, RoleRepresentation.class); final String[] fileNameParts = file.toString().split(PATH_SEPARATOR); final String realmName = fileNameParts[fileNameParts.length - 3]; @@ -34,9 +35,11 @@ protected RoleRepresentation importFile(final Path file) { .get(client.getId()) .roles() .create(role); - Log.infof("Client role '%s' imported.", role.getName()); + Log.infof("Client role '%s' imported for client '%s' of realm '%s'.", role.getName(), + clientId, realmName); } catch (final ClientErrorException e) { - Log.errorf("Could not import client role from file: %s", e.getMessage()); + Log.errorf("Could not import client role for client '%s' of realm '%s': %s", clientId, + realmName, e.getMessage()); } return keycloak.realm(realmName) diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/GroupImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/GroupImporter.java index c11c4a8..4cd6904 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/GroupImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/GroupImporter.java @@ -9,6 +9,7 @@ import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.GroupRepresentation; +import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; import io.quarkus.logging.Log; @@ -22,30 +23,31 @@ public EntityType getType() { @Override protected Object importFile(final Path file) { - final GroupRepresentation group = loadEntity(file, GroupRepresentation.class); + final GroupRepresentation group = JsonUtil.loadEntity(file, GroupRepresentation.class); final String[] fileNameParts = file.toString().split(PATH_SEPARATOR); - final String realmName = fileNameParts[fileNameParts.length - 2]; + final String realmName = fileNameParts[fileNameParts.length - 3]; try (final Response response = keycloak.realm(realmName) .groups() .add(group)) { if (response.getStatus() == 409) { - Log.errorf("Could not import group from file: %s", + Log.errorf("Could not import group for realm '%s': %s", realmName, response.readEntity(ErrorRepresentation.class) .getErrorMessage()); } else { - Log.infof("Group '%s' imported.", group.getName()); + Log.infof("Group '%s' imported for realm '%s'.", group.getName(), realmName); } } catch (final ClientErrorException e) { - Log.errorf("Could not import group from file: %s", e.getMessage()); + Log.errorf("Could not import group for realm '%s': %s", realmName, e.getMessage()); } final GroupRepresentation importedGroup = keycloak.realm(realmName) .groups() .query(group.getName()) .get(0); - Log.infof("Loaded imported group '%s' from server.", importedGroup.getName()); + Log.infof("Loaded imported group '%s' from realm '%s'.", importedGroup.getName(), + realmName); return importedGroup; } } diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmImporter.java index ed0ef81..68c2cff 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmImporter.java @@ -7,6 +7,7 @@ import org.keycloak.representations.idm.RealmRepresentation; +import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; import io.quarkus.logging.Log; @@ -20,7 +21,7 @@ public EntityType getType() { @Override protected RealmRepresentation importFile(final Path file) { - final RealmRepresentation realm = loadEntity(file, RealmRepresentation.class); + final RealmRepresentation realm = JsonUtil.loadEntity(file, RealmRepresentation.class); try { keycloak.realms() diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmRoleImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmRoleImporter.java index 4dc7dc5..7f8f6c5 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmRoleImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmRoleImporter.java @@ -7,6 +7,7 @@ import org.keycloak.representations.idm.RoleRepresentation; +import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; import io.quarkus.logging.Log; @@ -20,25 +21,25 @@ public EntityType getType() { @Override protected Object importFile(final Path file) { - final RoleRepresentation role = loadEntity(file, RoleRepresentation.class); + final RoleRepresentation role = JsonUtil.loadEntity(file, RoleRepresentation.class); final String[] fileNameParts = file.toString().split(PATH_SEPARATOR); - final String realmName = fileNameParts[fileNameParts.length - 2]; + final String realmName = fileNameParts[fileNameParts.length - 3]; try { keycloak.realm(realmName) .roles() .create(role); - Log.infof("Realm role '%s' imported.", role.getName()); + Log.infof("Realm role '%s' imported for realm '%s'.", role.getName(), realmName); } catch (final ClientErrorException e) { - Log.errorf("Could not import realm role from file: %s", e.getMessage()); + Log.errorf("Could not import realm role for realm '%s': %s", realmName, e.getMessage()); } final RoleRepresentation importedRole = keycloak.realm(realmName) .roles() .get(role.getName()) .toRepresentation(); - Log.infof("Loaded imported realm role '%s' from server.", importedRole.getName()); + Log.infof("Loaded imported realm role '%s' from realm '%s'.", importedRole.getName(), realmName); return importedRole; } } diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/UserImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/UserImporter.java index 2dd59f8..192d566 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/UserImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/UserImporter.java @@ -10,6 +10,7 @@ import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; import io.quarkus.logging.Log; @@ -23,23 +24,24 @@ public EntityType getType() { @Override protected Object importFile(final Path file) { - final UserRepresentation user = loadEntity(file, UserRepresentation.class); + final UserRepresentation user = JsonUtil.loadEntity(file, UserRepresentation.class); final String[] fileNameParts = file.toString().split(PATH_SEPARATOR); - final String realmName = fileNameParts[fileNameParts.length - 2]; + final String realmName = fileNameParts[fileNameParts.length - 3]; try (final Response response = keycloak.realm(realmName) .users() .create(user)) { if (response.getStatus() == 409) { - Log.errorf("Could not import user from file: %s", + Log.errorf("Could not import user from file for realm '%s': %s", realmName, response.readEntity(ErrorRepresentation.class) .getErrorMessage()); } else { - Log.infof("User '%s' imported.", user.getEmail()); + Log.infof("User '%s' imported for realm '%s'.", user.getEmail(), realmName); } } catch (final ClientErrorException e) { - Log.errorf("Could not import user from file: %s", e.getMessage()); + Log.errorf("Could not import user from file for realm '%s': %s", realmName, + e.getMessage()); } return loadImportedUser(realmName, user); @@ -51,11 +53,12 @@ private UserRepresentation loadImportedUser(final String realmName, .users() .search(user.getUsername()); if (searchResult.isEmpty()) { - Log.infof("Could not load imported user '%s' from server.", user.getUsername()); + Log.infof("Could not load imported user '%s' from realm '%s'.", user.getUsername(), + realmName); return null; } final UserRepresentation importedUser = searchResult.getFirst(); - Log.infof("Loaded imported user '%s' from server.", importedUser.getEmail()); + Log.infof("Loaded imported user '%s' from realm '%s'.", importedUser.getEmail(), realmName); return importedUser; } } diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/ConfigurationFileStore.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/ConfigurationFileStore.java new file mode 100644 index 0000000..0bc154b --- /dev/null +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/ConfigurationFileStore.java @@ -0,0 +1,89 @@ +package com.cycrilabs.keycloak.configurator.commands.configure.control; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import com.cycrilabs.keycloak.configurator.commands.configure.entity.ConfigureCommandConfiguration; +import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; + +import io.quarkus.logging.Log; + +@ApplicationScoped +public class ConfigurationFileStore { + @Inject + ConfigureCommandConfiguration configuration; + + private final Map> configurationFiles = new HashMap<>(); + + @PostConstruct + public void init() { + Log.infof("Initializing configuration file store."); + create(); + } + + private void create() { + final String configurationDirectory = configuration.getConfigDirectory(); + final Path configurationPath = Paths.get(configurationDirectory).toAbsolutePath(); + Log.infof("Creating configuration file store for directory '%s'.", configurationPath); + + final List allRealmDirectories = listDirectoriesInPath(configurationPath); + for (final Path realmConfiguration : allRealmDirectories) { + final Map realmConfigurationDirectories = + listDirectoriesInPath(realmConfiguration) + .stream() + .collect(Collectors.toMap( + path -> path.getName(path.getNameCount() - 1).toString(), + Function.identity())); + for (final EntityType entityType : EntityType.values()) { + for (final Map.Entry configurationEntry : realmConfigurationDirectories.entrySet()) { + Log.debugf("Reading files from '%s' for type '%s'.", + configurationEntry.getValue(), entityType); + if (configurationEntry.getKey().contains(entityType.getDirectory())) { + configurationFiles.computeIfAbsent(entityType, + key -> listFilesInPath(configurationEntry.getValue())); + } + } + } + } + } + + private List listDirectoriesInPath(final Path dir) { + try (final Stream stream = Files.list(dir)) { + return stream + .filter(Files::isDirectory) + .toList(); + } catch (final IOException e) { + Log.errorf("Could not read directory '%s'.", dir); + } + return Collections.emptyList(); + } + + private List listFilesInPath(final Path dir) { + try (final Stream stream = Files.list(dir)) { + return stream + .filter(Files::isRegularFile) + .filter(file -> file.toString().endsWith(".json")) + .toList(); + } catch (final IOException e) { + Log.errorf("Could not read directory '%s'.", dir); + } + return Collections.emptyList(); + } + + public List getImportFiles(final EntityType type) { + return configurationFiles.getOrDefault(type, Collections.emptyList()); + } +} diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/shared/control/JsonUtil.java b/src/main/java/com/cycrilabs/keycloak/configurator/shared/control/JsonUtil.java index e072c29..46ca311 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/shared/control/JsonUtil.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/shared/control/JsonUtil.java @@ -1,7 +1,14 @@ package com.cycrilabs.keycloak.configurator.shared.control; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + import lombok.NoArgsConstructor; +import io.quarkus.logging.Log; + /** * Helper class to convert JSON to/from objects. It uses the {@link JsonbFactory} to create a JSON-B * instance and the configured {@link jakarta.json.bind.Jsonb}. @@ -33,4 +40,37 @@ public static T fromJson(final String content, final Class dtoClass) { public static String toJson(final Object entity) { return JsonbFactory.getJsonb(true).toJson(entity); } + + /** + * Loads a JSON file from the given file path. + * + * @param filePath + * path to the JSON file + * @return JSON string + */ + public static String loadJsonFromPath(final Path filePath) { + try { + return Files.readString(filePath, StandardCharsets.UTF_8); + } catch (final IOException e) { + Log.errorf("Could not read file '%s'.", filePath); + throw new RuntimeException(e); + } + } + + /** + * Loads an entity from the given file path. The file is expected to be a JSON file. + * The JSON is converted to an object of the given class. + * + * @param filepath + * path to the JSON file + * @param dtoClass + * class of the object to convert to + * @param + * type of the object to convert to + * @return object of the given class + */ + public static T loadEntity(final Path filepath, final Class dtoClass) { + final String json = loadJsonFromPath(filepath); + return fromJson(json, dtoClass); + } } diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/shared/control/KeycloakFactory.java b/src/main/java/com/cycrilabs/keycloak/configurator/shared/control/KeycloakFactory.java index 55f80dd..1c403f7 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/shared/control/KeycloakFactory.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/shared/control/KeycloakFactory.java @@ -10,11 +10,14 @@ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class KeycloakFactory { + private static final String REALM_MASTER = "master"; + private static final String CLIENT_ID_ADMIN_CLI = "admin-cli"; + public static Keycloak create(final KeycloakConfiguration configuration) { return KeycloakBuilder.builder() .serverUrl(configuration.getServer()) - .realm("master") - .clientId("admin-cli") + .realm(REALM_MASTER) + .clientId(CLIENT_ID_ADMIN_CLI) .grantType(OAuth2Constants.PASSWORD) .username(configuration.getUsername()) .password(configuration.getPassword()) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 37dbac0..f07f340 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,7 +5,9 @@ quarkus.container-image.labels."org.opencontainers.image.description"=Keycloak c quarkus.container-image.labels."org.opencontainers.image.source"=https://github.com/CycriLabs/keycloak-configurator # set default log level -quarkus.log.level=ERROR +%dev.quarkus.log.level=INFO +%dev.quarkus.log.category."com.cycrilabs".level=DEBUG +%prod.quarkus.log.level=ERROR # set default banner quarkus.banner.path=banner.txt From 71659cd062ee1756014cbdc53b06357d7f140e68 Mon Sep 17 00:00:00 2001 From: Marc Scheib Date: Sun, 14 Jan 2024 22:52:47 +0100 Subject: [PATCH 2/2] feat: adapt importers to new configuration dir structure (#26) --- .../boundary/ClientRoleImporter.java | 5 +- .../configure/boundary/RealmRoleImporter.java | 21 +++-- .../control/ConfigurationFileStore.java | 92 +++++++++++++++---- .../configure/control/EntityStore.java | 8 +- 4 files changed, 98 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientRoleImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientRoleImporter.java index 37f8e19..1b89529 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientRoleImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ClientRoleImporter.java @@ -1,6 +1,7 @@ package com.cycrilabs.keycloak.configurator.commands.configure.boundary; import java.nio.file.Path; +import java.util.Arrays; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.ClientErrorException; @@ -25,7 +26,7 @@ protected RoleRepresentation importFile(final Path file) { final RoleRepresentation role = JsonUtil.loadEntity(file, RoleRepresentation.class); final String[] fileNameParts = file.toString().split(PATH_SEPARATOR); - final String realmName = fileNameParts[fileNameParts.length - 3]; + final String realmName = fileNameParts[fileNameParts.length - 4]; final String clientId = fileNameParts[fileNameParts.length - 2]; final ClientRepresentation client = entityStore.getClient(realmName, clientId); @@ -37,7 +38,7 @@ protected RoleRepresentation importFile(final Path file) { .create(role); Log.infof("Client role '%s' imported for client '%s' of realm '%s'.", role.getName(), clientId, realmName); - } catch (final ClientErrorException e) { + } catch (final Exception e) { Log.errorf("Could not import client role for client '%s' of realm '%s': %s", clientId, realmName, e.getMessage()); } diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmRoleImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmRoleImporter.java index 7f8f6c5..428b0e7 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmRoleImporter.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/RealmRoleImporter.java @@ -1,10 +1,12 @@ package com.cycrilabs.keycloak.configurator.commands.configure.boundary; import java.nio.file.Path; +import java.util.List; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.ClientErrorException; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; @@ -35,11 +37,18 @@ protected Object importFile(final Path file) { Log.errorf("Could not import realm role for realm '%s': %s", realmName, e.getMessage()); } - final RoleRepresentation importedRole = keycloak.realm(realmName) - .roles() - .get(role.getName()) - .toRepresentation(); - Log.infof("Loaded imported realm role '%s' from realm '%s'.", importedRole.getName(), realmName); - return importedRole; + try { + final RoleRepresentation importedRole = keycloak.realm(realmName) + .roles() + .get(role.getName()) + .toRepresentation(); + Log.infof("Loaded imported realm role '%s' from realm '%s'.", importedRole.getName(), + realmName); + return importedRole; + } catch (final ClientErrorException e) { + Log.errorf("Could not load imported realm role '%s' from realm '%s': %s", role.getName(), + realmName, e.getMessage()); + return null; + } } } diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/ConfigurationFileStore.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/ConfigurationFileStore.java index 0bc154b..01a54af 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/ConfigurationFileStore.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/ConfigurationFileStore.java @@ -21,6 +21,11 @@ import io.quarkus.logging.Log; +/** + * Store for configuration files that holds a list of all configuration files for each entity type. + * the configuration files are read from the configured directory and its subdirectories on first + * access. + */ @ApplicationScoped public class ConfigurationFileStore { @Inject @@ -34,6 +39,10 @@ public void init() { create(); } + /** + * Create the configuration file store by reading all configuration files from the configured + * directory and its subdirectories. + */ private void create() { final String configurationDirectory = configuration.getConfigDirectory(); final Path configurationPath = Paths.get(configurationDirectory).toAbsolutePath(); @@ -41,25 +50,18 @@ private void create() { final List allRealmDirectories = listDirectoriesInPath(configurationPath); for (final Path realmConfiguration : allRealmDirectories) { - final Map realmConfigurationDirectories = - listDirectoriesInPath(realmConfiguration) - .stream() - .collect(Collectors.toMap( - path -> path.getName(path.getNameCount() - 1).toString(), - Function.identity())); - for (final EntityType entityType : EntityType.values()) { - for (final Map.Entry configurationEntry : realmConfigurationDirectories.entrySet()) { - Log.debugf("Reading files from '%s' for type '%s'.", - configurationEntry.getValue(), entityType); - if (configurationEntry.getKey().contains(entityType.getDirectory())) { - configurationFiles.computeIfAbsent(entityType, - key -> listFilesInPath(configurationEntry.getValue())); - } - } - } + createTypeDirectoryLookup(realmConfiguration); + mapEntityTypes(createTypeDirectoryLookup(realmConfiguration)); } } + /** + * List all directories in the given directory. + * + * @param dir + * directory to list directories in + * @return list of directories in the given directory + */ private List listDirectoriesInPath(final Path dir) { try (final Stream stream = Files.list(dir)) { return stream @@ -71,8 +73,57 @@ private List listDirectoriesInPath(final Path dir) { return Collections.emptyList(); } + /** + * Create a lookup map for the given directory that maps all directories within the given one. + * The key is the name of the directory and the value is the full path to the directory. + * + * @param realmConfiguration + * directory to create lookup map for + * @return lookup map for the given directory + */ + private Map createTypeDirectoryLookup(final Path realmConfiguration) { + return listDirectoriesInPath(realmConfiguration) + .stream() + .collect(Collectors.toMap( + path -> path.getName(path.getNameCount() - 1).toString(), + Function.identity())); + } + + /** + * Map all entity types to their configuration files. + * + * @param typeDirectoryLookup + * lookup map for the realm configuration directory + */ + private void mapEntityTypes(final Map typeDirectoryLookup) { + for (final EntityType entityType : EntityType.values()) { + for (final Map.Entry configurationEntry : typeDirectoryLookup.entrySet()) { + final String potentialEntityTypeDirectory = configurationEntry.getKey(); + final Path fullEntityTypePath = configurationEntry.getValue(); + if (potentialEntityTypeDirectory.contains(entityType.getDirectory())) { + Log.debugf("Reading files from '%s' for type '%s'.", fullEntityTypePath, + entityType); + configurationFiles.computeIfAbsent(entityType, + key -> listFilesInPath(fullEntityTypePath)); + Log.debugf("Found %d files for type '%s'.", + configurationFiles.get(entityType).size(), entityType); + } else { + Log.debugf("Skipping directory '%s' for type '%s'.", fullEntityTypePath, + entityType); + } + } + } + } + + /** + * List all files in the given directory and its subdirectories that end with '.json'. + * + * @param dir + * directory to list files in + * @return list of files in the given directory and its subdirectories + */ private List listFilesInPath(final Path dir) { - try (final Stream stream = Files.list(dir)) { + try (final Stream stream = Files.walk(dir)) { return stream .filter(Files::isRegularFile) .filter(file -> file.toString().endsWith(".json")) @@ -83,6 +134,13 @@ private List listFilesInPath(final Path dir) { return Collections.emptyList(); } + /** + * Get all paths for the given entity type. + * + * @param type + * entity type + * @return list of files for the given entity type + */ public List getImportFiles(final EntityType type) { return configurationFiles.getOrDefault(type, Collections.emptyList()); } diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/EntityStore.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/EntityStore.java index 5659544..13f6189 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/EntityStore.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/control/EntityStore.java @@ -39,8 +39,10 @@ public void addClient(final String realmName, final ClientRepresentation importe } public ClientRepresentation getClient(final String realmName, final String clientId) { - return realms.get(realmName) - .getChildren() - .get(clientId); + return realms.get(realmName) != null + ? realms.get(realmName) + .getChildren() + .get(clientId) + : null; } }