diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ServiceAccountRealmRoleImporter.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ServiceAccountRealmRoleImporter.java new file mode 100644 index 0000000..d5f2f78 --- /dev/null +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/boundary/ServiceAccountRealmRoleImporter.java @@ -0,0 +1,93 @@ +package com.cycrilabs.keycloak.configurator.commands.configure.boundary; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import com.cycrilabs.keycloak.configurator.commands.configure.entity.ServiceUserRealmRoleMappingDTO; +import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil; +import com.cycrilabs.keycloak.configurator.shared.entity.EntityType; + +import io.quarkus.logging.Log; + +@ApplicationScoped +public class ServiceAccountRealmRoleImporter extends AbstractImporter { + @Override + public EntityType getType() { + return EntityType.SERVICE_ACCOUNT_REALM_ROLE; + } + + @Override + protected Object importFile(final Path file) { + final String[] fileNameParts = file.toString().split(PATH_SEPARATOR); + final String realmName = fileNameParts[fileNameParts.length - 4]; + final String serviceUsername = fileNameParts[fileNameParts.length - 2]; + + Log.debugf( + "Importing service account realm roles '%s' for service user '%s' of realm '%s'.", + file.getFileName(), serviceUsername, realmName); + + final UserRepresentation user = loadUserByUsername(realmName, serviceUsername); + if (user == null) { + return null; + } + + Log.debugf("Found service user '%s' of realm '%s'.", user.getUsername(), realmName); + + importServiceUserRealmRoleMappings(file, realmName, user); + + return null; + } + + private UserRepresentation loadUserByUsername(final String realmName, final String username) { + try { + final List userRepresentations = keycloak.realm(realmName) + .users() + .searchByUsername(username, Boolean.TRUE); + if (userRepresentations.size() == 1) { + return userRepresentations.getFirst(); + } + + Log.warnf("Found %d users '%s' of realm '%s'. Skipping import.", + Integer.valueOf(userRepresentations.size()), username, realmName); + } catch (final Exception e) { + Log.errorf("Could not find user '%s' of realm '%s': %s", username, realmName, + e.getMessage()); + } + return null; + } + + private void importServiceUserRealmRoleMappings(final Path file, final String realmName, + final UserRepresentation serviceUser) { + final ServiceUserRealmRoleMappingDTO serviceUserRealmRoleMappings = + JsonUtil.loadEntity(file, ServiceUserRealmRoleMappingDTO.class); + final List roles = serviceUserRealmRoleMappings.getRoles(); + + Log.debugf("Importing realm roles '%s' for service user '%s' of realm '%s'.", + roles.toString(), serviceUser.getUsername(), realmName); + final Map availableRealmRoles = keycloak.realm(realmName) + .roles() + .list() + .stream() + .collect(Collectors.toMap(RoleRepresentation::getName, Function.identity())); + Log.debugf("Found %d roles of realm '%s'.", Integer.valueOf(availableRealmRoles.size()), + realmName); + + keycloak.realm(realmName) + .users() + .get(serviceUser.getId()) + .roles() + .realmLevel() + .add(roles.stream() + .filter(availableRealmRoles::containsKey) + .map(availableRealmRoles::get) + .toList()); + } +} 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 f7f2d88..6c9fd2e 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 @@ -98,7 +98,7 @@ private void mapEntityTypes(final Map typeDirectoryLookup) { for (final Map.Entry configurationEntry : typeDirectoryLookup.entrySet()) { final String potentialEntityTypeDirectory = configurationEntry.getKey(); final Path fullEntityTypePath = configurationEntry.getValue(); - if (potentialEntityTypeDirectory.contains(entityType.getDirectory())) { + if (compareDirectoryNames(potentialEntityTypeDirectory, entityType)) { Log.debugf("Reading files from '%s' for type '%s'.", fullEntityTypePath, entityType); configurationFiles.computeIfAbsent(entityType, @@ -113,6 +113,27 @@ private void mapEntityTypes(final Map typeDirectoryLookup) { } } + /** + * Compare the given directory name with the directory name of the given entity type. + * + * @param potentialEntityTypeDirectory + * directory name to compare + * @param entityType + * entity type to compare + * @return true if the directory name contains the directory name of the entity type + */ + private boolean compareDirectoryNames(final String potentialEntityTypeDirectory, + final EntityType entityType) { + // some simple algorithm to compare if a directory matches an entity type, + // e.g. if it is prefixed by number: 1_realms and realms + // the overall length difference should not be that huge to avoid embedded naming + // e.g. client-roles and service-account-client-roles + final int maxNameLengthDiff = 5; + return potentialEntityTypeDirectory.contains(entityType.getDirectory()) + && potentialEntityTypeDirectory.length() - entityType.getDirectory().length() + < maxNameLengthDiff; + } + /** * List all files in the given directory and its subdirectories that end with '.json'. * diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/entity/ServiceUserRealmRoleMappingDTO.java b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/entity/ServiceUserRealmRoleMappingDTO.java new file mode 100644 index 0000000..9fe6ad4 --- /dev/null +++ b/src/main/java/com/cycrilabs/keycloak/configurator/commands/configure/entity/ServiceUserRealmRoleMappingDTO.java @@ -0,0 +1,12 @@ +package com.cycrilabs.keycloak.configurator.commands.configure.entity; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ServiceUserRealmRoleMappingDTO { + private List roles; +} diff --git a/src/main/java/com/cycrilabs/keycloak/configurator/shared/entity/EntityType.java b/src/main/java/com/cycrilabs/keycloak/configurator/shared/entity/EntityType.java index 5fbdff6..1c9cd40 100644 --- a/src/main/java/com/cycrilabs/keycloak/configurator/shared/entity/EntityType.java +++ b/src/main/java/com/cycrilabs/keycloak/configurator/shared/entity/EntityType.java @@ -12,7 +12,8 @@ public enum EntityType { REALM_ROLE(4, "realm-role", "realm-roles"), SERVICE_ACCOUNT_CLIENT_ROLE(5, "service-account-client-role", "service-account-client-roles"), GROUP(6, "group", "groups"), - USER(7, "user", "users"); + USER(7, "user", "users"), + SERVICE_ACCOUNT_REALM_ROLE(8, "service-account-realm-role", "service-account-realm-roles"); private final int priority; private final String name;