From 3cbbebd794c86210f13a0b972431899e8e142ac8 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Wed, 7 Jan 2026 12:43:59 +0100 Subject: [PATCH 01/15] ESB-890: Dynamic mapping of authorizations from KC user profile --- .../KeycloakAuthorizationManager.java | 404 ++++++++++++++++-- .../keycloak/services/KeycloakMapper.java | 12 +- .../keycloak/services/KeycloakService.java | 142 +++++- .../services/mapping/DynamicMapping.java | 13 + .../mapping/DynamicMappingElement.java | 18 + .../services/mapping/DynamicMappingKind.java | 32 ++ .../services/oidc/model/KeycloakUser.java | 37 ++ .../oidc/model/UserRepresentation.java | 11 + .../spring/plugins/keycloak/aps/keycloak.xml | 11 + .../KeycloakAuthorizationManagerTest.java | 215 +++++++++- 10 files changed, 817 insertions(+), 78 deletions(-) create mode 100644 keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java create mode 100644 keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java create mode 100644 keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java create mode 100644 keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/oidc/model/KeycloakUser.java diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index 417c6db9ba..67eb3c6557 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -1,60 +1,132 @@ package org.entando.entando.keycloak.services; +import static java.util.Optional.ofNullable; +import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.GROUP; +import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.GROUPROLE; +import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.ROLE; + +import com.agiletec.aps.system.common.AbstractService; import com.agiletec.aps.system.services.authorization.Authorization; import com.agiletec.aps.system.services.authorization.AuthorizationManager; +import com.agiletec.aps.system.services.baseconfig.BaseConfigManager; import com.agiletec.aps.system.services.group.Group; import com.agiletec.aps.system.services.group.GroupManager; import com.agiletec.aps.system.services.role.Role; import com.agiletec.aps.system.services.role.RoleManager; import com.agiletec.aps.system.services.user.UserDetails; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.common.collect.Sets; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; - -import static java.util.Optional.ofNullable; - +import org.apache.commons.lang3.StringUtils; import org.entando.entando.ent.exception.EntException; +import org.entando.entando.ent.util.EntLogging.EntLogFactory; +import org.entando.entando.ent.util.EntLogging.EntLogger; +import org.entando.entando.keycloak.services.mapping.DynamicMapping; +import org.entando.entando.keycloak.services.mapping.DynamicMappingElement; +import org.entando.entando.keycloak.services.oidc.model.KeycloakUser; +import org.springframework.beans.factory.annotation.Autowired; + +//@Service +public class KeycloakAuthorizationManager extends AbstractService { -@Service -public class KeycloakAuthorizationManager { + private static final EntLogger log = EntLogFactory.getSanitizedLogger(KeycloakAuthorizationManager.class); + private static final String DEFAULT_SEPARATOR = "_"; private final KeycloakConfiguration configuration; private final AuthorizationManager authorizationManager; private final GroupManager groupManager; private final RoleManager roleManager; + private final BaseConfigManager configManager; private static final int GROUP_POSITION = 0; private static final int ROLE_POSITION = 1; @Autowired public KeycloakAuthorizationManager(final KeycloakConfiguration configuration, - final AuthorizationManager authorizationManager, - final GroupManager groupManager, - final RoleManager roleManager) { + final AuthorizationManager authorizationManager, + final GroupManager groupManager, + final RoleManager roleManager, + final BaseConfigManager configManager1) { this.configuration = configuration; this.authorizationManager = authorizationManager; this.groupManager = groupManager; this.roleManager = roleManager; + this.configManager = configManager1; } - public void processNewUser(final UserDetails user) { - if (StringUtils.isEmpty(configuration.getDefaultAuthorizations())) { - return; + /** + * Dynamic mapping elements that are currently activeMappings based on their enabled status. + */ + private List activeMappings; + + @Override + public void init() throws Exception { + try { + String xml = configManager.getConfigItem("dynamicAuthMapping"); + if (StringUtils.isNotBlank(xml)) { + XmlMapper mapper = new XmlMapper(); + DynamicMapping dynConf = mapper.readValue(xml, DynamicMapping.class); + if (dynConf != null && dynConf.mapping != null) { + + activeMappings = dynConf.mapping + .stream() + .filter(this::isValid) + .filter(d -> (d.enabled != null && d.enabled)) + .collect(Collectors.toList()); + log.info("{} dynamic auth mapping found, {} activeMappings", + dynConf.mapping.size(), activeMappings.size()); + log.info("*** Dynamic mapping will be refreshed after configuration reload ***"); + } + } + } catch (Exception e) { + log.error("Error initializing KeycloakAuthorizationManager", e); + } + log.info("{} init completed", this.getClass()); + } + + /** + * Check whether the dynamic configuration element provided is valid + * @param elem the single dynamic mapping element to validate + * @return true if the element is valid, false otherwise + */ + private boolean isValid(DynamicMappingElement elem) { + if (StringUtils.isBlank(elem.attribute)) { + log.error("invalid dynamic mapping element, 'attribute' is blank"); + return false; + } + if (elem.kind == null) { + log.error("invalid dynamic mapping element, 'kind' is blank"); + return false; } - final Set defaultAuthorizations = Sets.newHashSet(configuration.getDefaultAuthorizations().split(",")); - final Set userAuthorizations = user.getAuthorizations().stream().map(authorization -> { - final String group = ofNullable(authorization.getGroup()).map(Group::getName).orElse(""); - final String role = ofNullable(authorization.getRole()).map(Role::getName).orElse(""); - return StringUtils.isEmpty(role) ? group : group + ":" + role; - }).collect(Collectors.toSet()); + if (elem.kind != ROLE && elem.kind != GROUP && elem.kind != GROUPROLE) { + log.error("invalid dynamic mapping element, kind '{}' is unknown", elem.kind); + return false; + } + return true; + } + + public void processNewUser(final UserDetails user) { + if (StringUtils.isNotEmpty(configuration.getDefaultAuthorizations())) { + // process group and role coming from the configuration + final Set defaultAuthorizations = Sets.newHashSet(configuration.getDefaultAuthorizations().split(",")); + final Set userAuthorizations = user.getAuthorizations().stream().map(authorization -> { + final String group = ofNullable(authorization.getGroup()).map(Group::getName).orElse(""); + final String role = ofNullable(authorization.getRole()).map(Role::getName).orElse(""); + return StringUtils.isEmpty(role) ? group : group + ":" + role; + }).collect(Collectors.toSet()); - defaultAuthorizations.stream() - .filter(defaultGroup -> !userAuthorizations.contains(defaultGroup)) - .forEach(authorization -> this.assignGroupToUser(authorization, user)); + defaultAuthorizations.stream() + .filter(defaultGroup -> !userAuthorizations.contains(defaultGroup)) + .forEach(authorization -> this.assignGroupToUser(authorization, user)); + } + // process mapping coming from the user profile + if (user instanceof KeycloakUser) { + processDynamicMapping((KeycloakUser) user); + } } private void assignGroupToUser(final String authorization, final UserDetails user) { @@ -66,43 +138,285 @@ private void assignGroupToUser(final String authorization, final UserDetails use .map(this::findOrCreateGroup).orElse(null); final Role role = ofNullable(roleName).filter(StringUtils::isNotEmpty) .map(this::findOrCreateRole).orElse(null); - groupName = ofNullable(group).map(Group::getName).orElse(null); // null or "" ? - roleName = ofNullable(role).map(Role::getName).orElse(null); // null or "" ? + groupName = ofNullable(group).map(Group::getName).orElse(null); // null or ""? + roleName = ofNullable(role).map(Role::getName).orElse(null); // null or ""? authorizationManager.addUserAuthorization(user.getUsername(), groupName, roleName); - user.addAuthorization(new Authorization(group, role)); } catch (EntException e) { throw new RuntimeException(e); } } private Group findOrCreateGroup(final String groupName) { - try { - Group group = groupManager.getGroup(groupName); - if (group == null) { - group = new Group(); - group.setName(groupName); - group.setDescription(groupName); + Group group = groupManager.getGroup(groupName); + if (group == null) { + group = new Group(); + group.setName(groupName); + group.setDescription(groupName); + try { groupManager.addGroup(group); + } catch (EntException e) { + log.error("Failed to create group: {}", groupName, e); } - return group; - } catch (EntException e) { - throw new RuntimeException(e); } + return group; } private Role findOrCreateRole(final String roleName) { - try { - Role role = roleManager.getRole(roleName); - if (role == null) { - role = new Role(); - role.setName(roleName); - role.setDescription(roleName); + Role role = roleManager.getRole(roleName); + if (role == null) { + role = new Role(); + role.setName(roleName); + role.setDescription(roleName); + try { roleManager.addRole(role); + } catch (EntException e) { + log.error("Failed to create role: {}", roleName, e); } - return role; - } catch (EntException e) { - throw new RuntimeException(e); } + return role; + } + + /** + * Map dynamically, optionally persisting, authorization coming from the user profile in + * keycloak + * @param user the currently logged user + */ + private synchronized void processDynamicMapping(final KeycloakUser user) { + if (activeMappings != null && !activeMappings.isEmpty()) { + + activeMappings.forEach(m -> { + if (m.kind == ROLE) { + doProcessRole(user, m); + } + if (m.kind == GROUP) { + doProcessGroup(user, m); + } + if (m.kind == GROUPROLE) { + doProcessGroupRole(user, m); + } + }); + } + } + + private void doProcessGroupRole(KeycloakUser user, DynamicMappingElement elem) { + final String separator = StringUtils.isBlank(elem.separator) ? + DEFAULT_SEPARATOR : elem.separator; + + try { + final List authorizations = processDynamicConfiguration(user, elem); + + if (authorizations == null) { + return; + } + for (String groupRoleToken : authorizations) { + final String[] tokens = groupRoleToken.split(separator); + if (tokens.length != 2 || StringUtils.isBlank(tokens[0]) || StringUtils.isBlank(tokens[1])) { + log.error("invalid dynamic config configuration detected"); + return; + } + final String groupName = tokens[0]; + final String roleName = tokens[1]; + + Authorization authorization; + Group group = null; + Role role = null; + + if (elem.persist) { + if (StringUtils.isNotBlank(groupName)) { + group = findOrCreateGroup(groupName); + } + + if (StringUtils.isNotBlank(roleName)) { + role = findOrCreateRole(roleName); + } + authorization = new Authorization(group, role); + + persistAuthIfMissing(user, authorization); + } else { + + if (StringUtils.isNotBlank(groupName)) { + group = new Group(); + group.setName(groupName); + group.setDescription("sys:" + groupName); + } + if (StringUtils.isNotBlank(roleName)) { + // make sure all the permissions are assigned to the current role + role = roleManager.getRole(roleName); + } + authorization = new Authorization(group, role); + } + user.addAuthorization(authorization); + } + } catch (Exception e) { + log.error("error processing dynamic GRUOPROLE association", e); + } + } + + /** + * Process the dynamic Role authorization for the given user + * @param user the currently logging-in user + * @param elem a single dynamic configuration + */ + private void doProcessRole(KeycloakUser user, DynamicMappingElement elem) { + final List authorizations = processDynamicConfiguration(user, elem); + if (authorizations == null) { + return; + } + for (String kca: authorizations) { + try { + // skip if the group is already mapped + if (user.getAuthorizations() + .stream() + .anyMatch(a -> a.getRole() != null + && a.getRole().getName().equals(kca))) { + log.debug("Role {} already assigned to user {}", kca, user.getUsername()); + return; + } + final Authorization auth = elem.persist + ? createPersistedRoleAuthorization(user, kca) + : createTransientRoleAuthorization(kca); + + user.addAuthorization(auth); + log.info("Successfully assigned role {} to user {}", kca, user.getUsername()); + } catch (Exception e) { + log.error("Error processing dynamic role '{}' for user {}", kca , user.getUsername(), e); + } + } + } + + private Authorization createPersistedRoleAuthorization(KeycloakUser user, String roleName) throws EntException { + Role role = findOrCreateRole(roleName); + Authorization auth= new Authorization(null, role); + persistAuthIfMissing(user, auth); + return auth; + } + + private Authorization createTransientRoleAuthorization(String roleName) { + Role role = roleManager.getRole(roleName); + if (role == null) { + role = new Role(); + role.setName(roleName); + } + return new Authorization(null, role); + } + + /** + * Process the dynamic Group authorization for the given user + * @param user the currently logging-in user + * @param elem a single dynamic configuration + */ + private void doProcessGroup(KeycloakUser user, DynamicMappingElement elem) { + final List authorizations = processDynamicConfiguration(user, elem); + if (authorizations == null) { + return; + } + for (String kca: authorizations) { + try { + // skip if the role is already mapped + if (user.getAuthorizations() + .stream() + .anyMatch(a -> a.getGroup() != null + && a.getGroup().getName().equals(kca))) { + log.debug("Group {} already assigned to user {}", kca, user.getUsername()); + return; + } + final Authorization auth = elem.persist + ? createPersistedGroupAuthorization(user, kca) + : createTransientGroupAuthorization(kca); + + user.addAuthorization(auth); + log.info("Successfully assigned group {} to user {}", kca, user.getUsername()); + } catch (Exception e) { + log.error("Error processing dynamic group for user {}", user.getUsername(), e); + } + } + } + + private Authorization createPersistedGroupAuthorization(KeycloakUser user, String groupName) throws EntException { + final Group group = findOrCreateGroup(groupName); + final Authorization auth = new Authorization(group, null); + persistAuthIfMissing(user, auth); + return auth; + } + + private Authorization createTransientGroupAuthorization(String groupName) { + Group group = new Group(); + group.setName(groupName); + return new Authorization(group, null); + } + + /** + * Process dynamic configuration element for a user. If the attribute is missing, it skips processing. + * @param user the Keycloak user + * @param elem the dynamic mapping element + * @return the list of processed attribute tokens or null if the attribute is missing + */ + private static List processDynamicConfiguration(KeycloakUser user, DynamicMappingElement elem) { + if (user.getUserRepresentation() == null + || user.getUserRepresentation().getAttributes() == null + || !user.getUserRepresentation().getAttributes().containsKey(elem.attribute)) { + log.info("skipping dynamic processing for user {}", user.getUsername()); + return null; + } + final Object kcProfileAttr = user.getUserRepresentation() + .getAttributes() + .get(elem.attribute); + return handleKeycloakAttribute(kcProfileAttr); + } + + /** + * To avoid creating duplicate records, we are forced to check if the authorization already exists. + * @param user the user being processed + * @param auth the authorization to persist + * @throws EntException in case of errors + */ + private void persistAuthIfMissing(KeycloakUser user, Authorization auth) throws EntException { + final List existing = authorizationManager.getUserAuthorizations(user.getUsername()); + if (existing.stream() + .noneMatch(a -> (a.getGroup() != null && a.getRole() == null + && auth.getGroup() != null + && a.getGroup().getName().equals(auth.getGroup().getName())) + || + (a.getRole() != null && a.getGroup() == null + && auth.getRole() != null + && a.getRole().getName().equals(auth.getRole().getName())) + + || + (a.getRole() != null && a.getGroup() != null + && auth.getRole() != null + && auth.getGroup() != null + && a.getRole().getName().equals(auth.getRole().getName()) + && a.getGroup().getName().equals(auth.getGroup().getName())) + ) + ) { + log.error("dynamically persisting authorization for user '{}' : group {}, role {}", user.getUsername(), + auth.getGroup() != null ? auth.getGroup().getName() : "N/A", + auth.getRole() != null ? auth.getRole().getName() : "N/A"); + authorizationManager.addUserAuthorization(user.getUsername(), auth); + } + } + + /** + * Process user attribute of the Keycloak profile. If it's a list, it will be flattened and split by whitespace. + * If it's a string, it will be split by whitespace. + * @param attribute the attribute data + * @return the list of the processed attribute tokens + */ + protected static List handleKeycloakAttribute(Object attribute) { + if (attribute instanceof List) { + List list = (List) attribute; + return list.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .flatMap(s -> Arrays.stream(s.split("\\s+"))) + .filter(token -> !token.isBlank()) + .collect(Collectors.toUnmodifiableList()); + } else if (attribute instanceof String) { + return Arrays.stream(((String) attribute).split("\\s+")) + .filter(token -> !token.isBlank()) + .collect(Collectors.toUnmodifiableList()); + } + return new ArrayList<>(); } } diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakMapper.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakMapper.java index d86f437ac4..574ee54dcd 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakMapper.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakMapper.java @@ -1,23 +1,25 @@ package org.entando.entando.keycloak.services; +import static java.util.Optional.ofNullable; + import com.agiletec.aps.system.services.user.User; +import org.entando.entando.keycloak.services.oidc.model.KeycloakUser; import org.entando.entando.keycloak.services.oidc.model.UserRepresentation; -import static java.util.Optional.ofNullable; - class KeycloakMapper { static User convertUserDetails(final UserRepresentation userRepresentation) { final boolean credentialsExpired = ofNullable(userRepresentation.getRequiredActions()) .filter(actions -> actions.contains("UPDATE_PASSWORD")).isPresent(); - final User user = credentialsExpired ? newUserCredentialsExpired() : new User(); + final KeycloakUser user = credentialsExpired ? newUserCredentialsExpired() : new KeycloakUser(); user.setDisabled(!userRepresentation.isEnabled()); user.setUsername(userRepresentation.getUsername()); + user.setUserRepresentation(userRepresentation); return user; } - private static User newUserCredentialsExpired() { - return new User() { + private static KeycloakUser newUserCredentialsExpired() { + return new KeycloakUser() { { setMaxMonthsSinceLastAccess(-1); setMaxMonthsSinceLastPasswordChange(-1); diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java index b0d3bd9e7e..d140348851 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java @@ -1,13 +1,10 @@ package org.entando.entando.keycloak.services; -import static org.entando.entando.KeycloakWiki.wiki; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.entando.entando.KeycloakWiki; import org.entando.entando.aps.system.exception.RestServerError; @@ -27,6 +24,16 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.entando.entando.KeycloakWiki.wiki; + +@Slf4j @Service public class KeycloakService { @@ -34,6 +41,7 @@ public class KeycloakService { private final KeycloakConfiguration configuration; private final RestTemplate restTemplate; + @Autowired public KeycloakService(final KeycloakConfiguration configuration, final OpenIDConnectService oidcService, @Qualifier("keycloakRestTemplate")final RestTemplate rest) { this.configuration = configuration; @@ -45,17 +53,74 @@ public List listUsers() { return listUsers(null); } - public List listUsers(final String text) { +// public List listUsers(final String text) { +// final String url = String.format("%s/admin/realms/%s/users", configuration.getAuthUrl(), configuration.getRealm()); +// final Map params = StringUtils.isEmpty(text) +// ? Collections.emptyMap() +// : Collections.singletonMap("username", text); +// String token = this.extractToken(); +// final ResponseEntity response = this.executeRequest(token, url, +// HttpMethod.GET, createEntity(token), UserRepresentation[].class, params); +// return Optional.ofNullable(response.getBody()) +// .map(Arrays::asList) +// .orElse(Collections.emptyList()); +// } + + + // TODO http://localhost:8081/auth/admin/realms/entando-development/users?briefRepresentation=true&first=0&max=20&search=testutentemariorossi%2B4375@gmail.com + public List listUsers(String text) { final String url = String.format("%s/admin/realms/%s/users", configuration.getAuthUrl(), configuration.getRealm()); - final Map params = StringUtils.isEmpty(text) + final String searchString = StringUtils.isNotBlank(text) ? encodeForKeycloakSearchAPI(text) : text; + final boolean isExact = StringUtils.isBlank(text) || (StringUtils.isNotBlank(text) && searchString.equals(text)); + final Map params = StringUtils.isBlank(text) ? Collections.emptyMap() - : Collections.singletonMap("username", text); + : new HashMap<>() {{ + put("username", searchString); + }}; + List retval = null; String token = this.extractToken(); - final ResponseEntity response = this.executeRequest(token, url, - HttpMethod.GET, createEntity(token), UserRepresentation[].class, params); - return Optional.ofNullable(response.getBody()) - .map(Arrays::asList) - .orElse(Collections.emptyList()); + + try { + if (!isExact && !params.isEmpty()) { + params.put("briefRepresentation", "true"); + params.put("search", searchString); +// params.put("first", "0"); +// params.put("max", "20"); + params.remove("username"); + } + final ResponseEntity response = this.executeEscapedRequest(token, url, + HttpMethod.GET, createEntity(token, null), UserRepresentation[].class, params, 0); + retval = Optional.ofNullable(response.getBody()) + .map(Arrays::asList) + .orElse(Collections.emptyList()); + if (retval.size() > 1) { + // must match the exact element by USERNAME + Optional userOpt = retval.stream() + .filter(e -> (e.getUsername() != null && e.getUsername().equals(text)) + || (e.getEmail() != null && e.getEmail().equals(text))) + .findFirst(); + return userOpt.stream().collect(Collectors.toList()); + } + } catch (Exception e) { + log.error("Error listing users from Keycloak: {}", e.getMessage(), e); + } + return retval; + } + + private String encodeForKeycloakSearchAPI(String text) { + final List specialCharacters = List.of( + '#', '+', '&', '%', '\'', '/', '=', '?', '^', '{', '|', '}', '`', '"' + ); + final StringBuilder encoded = new StringBuilder(); + + for (char c : text.toCharArray()) { + if (specialCharacters.contains(c)) { + encoded.append(URLEncoder.encode(String.valueOf(c), StandardCharsets.UTF_8)); + } else { + encoded.append(c); + } + } + return encoded.toString(); } public void removeUser(final String uuid) { @@ -132,6 +197,51 @@ private ResponseEntity executeRequest(String token, final String url, } } + private ResponseEntity executeEscapedRequest(String token, final String url, final HttpMethod method, final HttpEntity entity, + final Class result, final Map params, int retryCount) { + try { + final UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url); + final Optional isAlreadyEscaped = params.values() + .stream().filter(v -> StringUtils.isNotBlank(v) && v.contains("%")) + .findFirst(); + if (isAlreadyEscaped.isEmpty()) { + // default behaviour, query string is escaped automatically + params.forEach(builder::queryParam); + return restTemplate.exchange(builder.build().toUri(), method, + createEntity(token, entity.getBody()), result); + } else { + /* + * We don't want to automatically escape character since they come + * already escaped, so we construct URI directly from the string + */ + StringBuilder queryBuilder = new StringBuilder(); + params.forEach((key, value) -> { + if (queryBuilder.length() > 0) { + queryBuilder.append("&"); + } + queryBuilder.append(key).append("=").append(value); + }); + String escapedUrl = builder.build().toUri() + "?" + queryBuilder; + +// restTemplate.setInterceptors(Collections.singletonList(new LoggingInterceptor())); + ResponseEntity retval = restTemplate.exchange( + URI.create(escapedUrl), method, createEntity(token, entity.getBody()), + result); + return retval; + } + } catch (HttpClientErrorException e) { + if (HttpStatus.FORBIDDEN.equals(e.getStatusCode()) || (HttpStatus.UNAUTHORIZED.equals(e.getStatusCode()) && retryCount > 10)) { + throw new RestServerError("There was an error while trying to load user because the " + + "client on Keycloak doesn't have permission to do that. " + + "The client needs to have Service Accounts enabled and the permission 'realm-admin' on client 'realm-management'. ", e); + } + if (HttpStatus.UNAUTHORIZED.equals(e.getStatusCode())) { + return this.executeEscapedRequest(null, url, method, entity, result, params, retryCount + 1); + } + throw e; + } + } + private String extractToken() { try { final AuthResponse authResponse = oidcService.authenticateAPI(); diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java new file mode 100644 index 0000000000..6606f7296a --- /dev/null +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java @@ -0,0 +1,13 @@ +package org.entando.entando.keycloak.services.mapping; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import java.util.List; + +@JacksonXmlRootElement(localName = "dynamicmapping") +public class DynamicMapping { + + @JacksonXmlElementWrapper(useWrapping = false) + public List mapping; + +} diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java new file mode 100644 index 0000000000..dd54e062d3 --- /dev/null +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java @@ -0,0 +1,18 @@ +package org.entando.entando.keycloak.services.mapping; + + +public class DynamicMappingElement { + + public Boolean enabled; + public String attribute; + public DynamicMappingKind kind; + public String injectTo; + public Boolean persist; + public String separator; + + public String toString() { + return "DynamicMappingElement(enabled=" + this.enabled + ", attribute=" + this.attribute + ", kind=" + + this.kind + ", injectTo=" + this.injectTo + + ", persist=" + this.persist + ", separator=\" + this.separator + \")"; + } +} diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java new file mode 100644 index 0000000000..5a1206397e --- /dev/null +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java @@ -0,0 +1,32 @@ +package org.entando.entando.keycloak.services.mapping; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Arrays; + +public enum DynamicMappingKind { + + GROUP("group"), + ROLE("role"), + GROUPROLE("grouprole"); + + public String kind; + DynamicMappingKind(String kind) { + this.kind = kind; + } + + @JsonValue + public String getXmlValue() { + return kind; + } + + @JsonCreator + public static DynamicMappingKind fromValue(String value) { + return Arrays.stream(values()) + .filter(k -> k.kind.equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> + new IllegalArgumentException("Unknown DynamicMappingKind: " + value)); + } + +} diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/oidc/model/KeycloakUser.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/oidc/model/KeycloakUser.java new file mode 100644 index 0000000000..acc92afccc --- /dev/null +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/oidc/model/KeycloakUser.java @@ -0,0 +1,37 @@ +package org.entando.entando.keycloak.services.oidc.model; + +import com.agiletec.aps.system.services.user.User; + +public class KeycloakUser extends User { + + private UserRepresentation userRepresentation; + + @Override + public boolean isEntandoUser() { + return false; + } + + @Override + @Deprecated + public boolean isJapsUser() { + return this.isEntandoUser(); + } + + @Override + public Object clone() { + KeycloakUser cl = new KeycloakUser(); + cl.setUsername(this.getUsername()); + cl.setPassword(""); + cl.setAuthorizations(this.getAuthorizations()); + cl.setUserRepresentation(this.getUserRepresentation()); + return cl; + } + + public UserRepresentation getUserRepresentation() { + return userRepresentation; + } + public void setUserRepresentation(UserRepresentation userRepresentation) { + this.userRepresentation = userRepresentation; + } + +} diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/oidc/model/UserRepresentation.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/oidc/model/UserRepresentation.java index 47778d8990..346a827f75 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/oidc/model/UserRepresentation.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/oidc/model/UserRepresentation.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.util.List; +import java.util.Map; public class UserRepresentation implements Serializable { @@ -15,6 +16,16 @@ public class UserRepresentation implements Serializable { private String lastName; private String email; private List requiredActions; + // ESB-890 + private Map attributes; + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } public String getId() { return id; diff --git a/keycloak-plugin/src/main/resources/spring/plugins/keycloak/aps/keycloak.xml b/keycloak-plugin/src/main/resources/spring/plugins/keycloak/aps/keycloak.xml index f4b00a5d58..9dc5815aed 100644 --- a/keycloak-plugin/src/main/resources/spring/plugins/keycloak/aps/keycloak.xml +++ b/keycloak-plugin/src/main/resources/spring/plugins/keycloak/aps/keycloak.xml @@ -7,6 +7,17 @@ + + + + + + + + + + diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java index 0dfecfc77c..faa44f89b1 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java @@ -2,11 +2,15 @@ import com.agiletec.aps.system.services.authorization.Authorization; import com.agiletec.aps.system.services.authorization.AuthorizationManager; +import com.agiletec.aps.system.services.baseconfig.BaseConfigManager; import com.agiletec.aps.system.services.group.Group; import com.agiletec.aps.system.services.group.GroupManager; import com.agiletec.aps.system.services.role.Role; import com.agiletec.aps.system.services.role.RoleManager; -import com.agiletec.aps.system.services.user.UserDetails; +import java.util.List; +import java.util.Map; +import org.entando.entando.keycloak.services.oidc.model.KeycloakUser; +import org.entando.entando.keycloak.services.oidc.model.UserRepresentation; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -17,6 +21,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -30,17 +36,18 @@ @ExtendWith(MockitoExtension.class) class KeycloakAuthorizationManagerTest { - @Mock private UserDetails userDetails; + @Mock private KeycloakUser userDetails; @Mock private KeycloakConfiguration configuration; @Mock private AuthorizationManager authorizationManager; @Mock private GroupManager groupManager; @Mock private RoleManager roleManager; + @Mock private BaseConfigManager configManager; private KeycloakAuthorizationManager manager; @BeforeEach - public void setUp() { - manager = new KeycloakAuthorizationManager(configuration, authorizationManager, groupManager, roleManager); + public void setUp() throws Exception { + manager = new KeycloakAuthorizationManager(configuration, authorizationManager, groupManager, roleManager, configManager); } @Test @@ -52,17 +59,23 @@ void testGroupCreation() throws EntException { manager.processNewUser(userDetails); final ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(Group.class); - final ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(Authorization.class); + final ArgumentCaptor auth_man_usernameCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor auth_man_groupCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor auth_man_roleCaptor = ArgumentCaptor.forClass(String.class); + verify(roleManager, times(0)).getRole(anyString()); verify(groupManager, times(1)).getGroup(eq("readers")); verify(groupManager, times(1)).addGroup(groupCaptor.capture()); - verify(userDetails, times(1)).addAuthorization(authorizationCaptor.capture()); + verify(authorizationManager).addUserAuthorization(auth_man_usernameCaptor.capture(), + auth_man_groupCaptor.capture(),auth_man_roleCaptor.capture()); + + assertThat(groupCaptor.getValue().getName()).isEqualTo("readers"); assertThat(groupCaptor.getValue().getName()).isEqualTo("readers"); assertThat(groupCaptor.getValue().getDescription()).isEqualTo("readers"); - assertThat(authorizationCaptor.getValue().getGroup().getName()).isEqualTo("readers"); - assertThat(authorizationCaptor.getValue().getRole()).isNull(); + assertThat(auth_man_groupCaptor.getValue()).isEqualTo("readers"); + assertThat(auth_man_roleCaptor.getValue()).isNull(); } @Test @@ -76,12 +89,16 @@ void testGroupAndRoleCreation() throws EntException { final ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(Group.class); final ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(Role.class); - final ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(Authorization.class); + final ArgumentCaptor auth_man_usernameCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor auth_man_groupCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor auth_man_roleCaptor = ArgumentCaptor.forClass(String.class); + verify(roleManager, times(1)).getRole(eq("read-all")); verify(roleManager, times(1)).addRole(roleCaptor.capture()); verify(groupManager, times(1)).getGroup(eq("readers")); verify(groupManager, times(1)).addGroup(groupCaptor.capture()); - verify(userDetails, times(1)).addAuthorization(authorizationCaptor.capture()); + verify(authorizationManager).addUserAuthorization(auth_man_usernameCaptor.capture(), + auth_man_groupCaptor.capture(), auth_man_roleCaptor.capture()); assertThat(groupCaptor.getValue().getName()).isEqualTo("readers"); assertThat(groupCaptor.getValue().getDescription()).isEqualTo("readers"); @@ -89,8 +106,8 @@ void testGroupAndRoleCreation() throws EntException { assertThat(roleCaptor.getValue().getName()).isEqualTo("read-all"); assertThat(roleCaptor.getValue().getDescription()).isEqualTo("read-all"); - assertThat(authorizationCaptor.getValue().getGroup().getName()).isEqualTo("readers"); - assertThat(authorizationCaptor.getValue().getRole().getName()).isEqualTo("read-all"); + assertThat(auth_man_groupCaptor.getValue()).isEqualTo("readers"); + assertThat(auth_man_roleCaptor.getValue()).isEqualTo("read-all"); } @Test @@ -108,6 +125,115 @@ void testVerification() { verify(userDetails, times(0)).addAuthorization(any()); } + @Test + void testDynamicConfigurationRoleOnLogin() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); +// when(groupManager.getGroup(anyString())).thenReturn(null); + when(roleManager.getRole(anyString())).thenReturn(null); + when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + when(configManager.getConfigItem(anyString())).thenReturn(XML_ROLE_CONF); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_ROLE", List.of("ruolo"))); + + when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails); + + verify(authorizationManager, times(1)).addUserAuthorization(isNull(), authCaptor.capture()); + + assertThat(authCaptor.getValue().getRole().getName()).isEqualTo("ruolo"); + assertThat(authCaptor.getValue().getGroup()).isNull(); + } + + @Test + void testDynamicConfigurationGroupOnLogin() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_GROUP_CONF); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUP", List.of("group"))); + + when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails); + + verify(authorizationManager, times(1)).addUserAuthorization(isNull(), authCaptor.capture()); + + assertThat(authCaptor.getValue().getGroup().getName()).isEqualTo("group"); + assertThat(authCaptor.getValue().getRole()).isNull(); + } + + @Test + void testDynamicConfigurationGroupRoleOnLogin() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_GROUP_ROLE_CONF); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("agroup_r_arole"))); + + when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails); + + verify(authorizationManager, times(1)).addUserAuthorization(isNull(), authCaptor.capture()); + + assertThat(authCaptor.getValue().getGroup().getName()).isEqualTo("agroup"); + assertThat(authCaptor.getValue().getRole().getName()).isEqualTo("arole"); + } + + @Test + void testDynamicConfigurationNoGroupOnlyRoleOnLogin() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_GROUP_ROLE_CONF); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("_r_arole"))); + + when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails); + + verify(authorizationManager, never()).addUserAuthorization(isNull(), any()); + } + + @Test + void testDynamicConfigurationOnlyGroupNoRoleOnLogin() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_GROUP_ROLE_CONF); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("group_r_"))); + + when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails); + + verify(authorizationManager, never()).addUserAuthorization(isNull(), any()); + } + + private Authorization authorization(final String groupName, final String roleName) { final Group group = new Group(); group.setName(groupName); @@ -116,4 +242,69 @@ private Authorization authorization(final String groupName, final String roleNam return new Authorization(group, role); } + private static final String XML_ROLE_CONF = "" + + " " + + " true" + + " AD_ROLE" + + " ROLE" + + " true" + + " " + + " " + + " false" + + " AD_GROUP" + + " GROUP" + + " true" + + " " + + " " + + " false" + + " AD_GROUPROLE" + + " GROUPROLE" + + " _r_" + + " true " + + " " + + ""; + + private static final String XML_GROUP_CONF = "" + + " " + + " false" + + " AD_ROLE" + + " ROLE" + + " true" + + " " + + " " + + " true" + + " AD_GROUP" + + " GROUP" + + " true" + + " " + + " " + + " false" + + " AD_GROUPROLE" + + " GROUPROLE" + + " _r_" + + " true " + + " " + + ""; + + private static final String XML_GROUP_ROLE_CONF = "" + + " " + + " false" + + " AD_ROLE" + + " ROLE" + + " true" + + " " + + " " + + " false" + + " AD_GROUP" + + " GROUP" + + " true" + + " " + + " " + + " true" + + " AD_GROUPROLE" + + " GROUPROLE" + + " _r_" + + " true" + + " " + + ""; } From ee2f17e3f192025123285e59e1ee4b237b3672d8 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Wed, 7 Jan 2026 13:04:16 +0100 Subject: [PATCH 02/15] ESB-890: code cleaning --- .../keycloak/services/KeycloakService.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java index d140348851..06ee040dbc 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java @@ -53,21 +53,7 @@ public List listUsers() { return listUsers(null); } -// public List listUsers(final String text) { -// final String url = String.format("%s/admin/realms/%s/users", configuration.getAuthUrl(), configuration.getRealm()); -// final Map params = StringUtils.isEmpty(text) -// ? Collections.emptyMap() -// : Collections.singletonMap("username", text); -// String token = this.extractToken(); -// final ResponseEntity response = this.executeRequest(token, url, -// HttpMethod.GET, createEntity(token), UserRepresentation[].class, params); -// return Optional.ofNullable(response.getBody()) -// .map(Arrays::asList) -// .orElse(Collections.emptyList()); -// } - - - // TODO http://localhost:8081/auth/admin/realms/entando-development/users?briefRepresentation=true&first=0&max=20&search=testutentemariorossi%2B4375@gmail.com + // Handle invocations such as http://localhost:8081/auth/admin/realms/entando-development/users?briefRepresentation=true&first=0&max=20&search=testutentemariorossi%2B4375@gmail.com public List listUsers(String text) { final String url = String.format("%s/admin/realms/%s/users", configuration.getAuthUrl(), configuration.getRealm()); final String searchString = StringUtils.isNotBlank(text) ? encodeForKeycloakSearchAPI(text) : text; From 5965af8facf0849b71fb8ea687eda201a5bb923c Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Thu, 8 Jan 2026 15:27:44 +0100 Subject: [PATCH 03/15] ESB-890: minor refactor --- .../keycloak/services/KeycloakAuthorizationManager.java | 2 +- .../entando/keycloak/services/KeycloakUserManager.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index 67eb3c6557..06867deafe 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -389,7 +389,7 @@ private void persistAuthIfMissing(KeycloakUser user, Authorization auth) throws && a.getGroup().getName().equals(auth.getGroup().getName())) ) ) { - log.error("dynamically persisting authorization for user '{}' : group {}, role {}", user.getUsername(), + log.info("dynamically persisting authorization for user '{}' : group {}, role {}", user.getUsername(), auth.getGroup() != null ? auth.getGroup().getName() : "N/A", auth.getRole() != null ? auth.getRole().getName() : "N/A"); authorizationManager.addUserAuthorization(user.getUsername(), auth); diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakUserManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakUserManager.java index 59d08f7ff0..39146d8bc5 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakUserManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakUserManager.java @@ -162,7 +162,11 @@ private void updateUserPassword(final UserRepresentation user, final String pass } private Optional getUserRepresentation(final String username) { - return keycloakService.listUsers(username).stream().filter(f->f.getUsername().equals(username)).findFirst(); + return ofNullable(keycloakService.listUsers(username)) + .orElse(emptyList()) + .stream() + .filter(f->f.getUsername().equals(username)) + .findFirst(); } @Override From 694de63394395911841808375d82f810b9a2b6c7 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Wed, 14 Jan 2026 08:58:57 +0100 Subject: [PATCH 04/15] ESB-890: code review (WIP) --- .../entity/AbstractEntitySearcherDAO.java | 5 +- .../KeycloakAuthenticationFilter.java | 5 +- .../keycloak/filter/KeycloakFilter.java | 2 +- .../KeycloakAuthorizationManager.java | 160 +++++++++++++----- .../mapping/DynamicMappingElement.java | 3 +- .../services/mapping/DynamicMappingKind.java | 3 +- .../spring/plugins/keycloak/aps/keycloak.xml | 15 +- .../KeycloakAuthorizationManagerTest.java | 150 +++++++++++++--- 8 files changed, 276 insertions(+), 67 deletions(-) diff --git a/engine/src/main/java/com/agiletec/aps/system/common/entity/AbstractEntitySearcherDAO.java b/engine/src/main/java/com/agiletec/aps/system/common/entity/AbstractEntitySearcherDAO.java index cbb25efb0c..06fa38e86e 100644 --- a/engine/src/main/java/com/agiletec/aps/system/common/entity/AbstractEntitySearcherDAO.java +++ b/engine/src/main/java/com/agiletec/aps/system/common/entity/AbstractEntitySearcherDAO.java @@ -136,6 +136,9 @@ protected EntitySearchFilter[] addFilter(EntitySearchFilter[] filters, EntitySea private PreparedStatement buildStatement(EntitySearchFilter[] filters, boolean isCount, boolean selectAll, Connection conn) { String query = this.createQueryString(filters, isCount, selectAll); + + System.out.println("\n>>> " + query); + PreparedStatement stat = null; try { stat = conn.prepareStatement(query); @@ -567,4 +570,4 @@ protected String getMasterTableName() { return this.getEntityMasterTableName(); } -} \ No newline at end of file +} diff --git a/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakAuthenticationFilter.java b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakAuthenticationFilter.java index 16261165a5..4b1a7f4c36 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakAuthenticationFilter.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakAuthenticationFilter.java @@ -87,6 +87,7 @@ public Authentication attemptAuthentication(final HttpServletRequest request, fi } final String bearerToken = authorization.substring("Bearer ".length()); + final ResponseEntity resp = oidcService.validateToken(bearerToken); final AccessToken accessToken = resp.getBody(); @@ -111,7 +112,7 @@ public Authentication attemptAuthentication(final HttpServletRequest request, fi setUserOnContext(request, user, userAuthentication); // TODO optimise to not check on every request - keycloakGroupManager.processNewUser(user); + keycloakGroupManager.processNewUser(user, bearerToken, true); return userAuthentication; } catch (EntException e) { @@ -174,4 +175,4 @@ public void onAuthenticationFailure(HttpServletRequest request, response.addHeader("Content-Type", "application/json"); response.getOutputStream().println(objectMapper.writeValueAsString(restResponse)); } -} \ No newline at end of file +} diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/filter/KeycloakFilter.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/filter/KeycloakFilter.java index bda6cff8b4..ccc7e824fd 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/filter/KeycloakFilter.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/filter/KeycloakFilter.java @@ -256,7 +256,7 @@ private void doLogin(final HttpServletRequest request, final HttpServletResponse session.setAttribute(SESSION_PARAM_ID_TOKEN, responseEntity.getBody().getIdToken()); session.setAttribute(SESSION_PARAM_REFRESH_TOKEN, responseEntity.getBody().getRefreshToken()); - keycloakGroupManager.processNewUser(user); + keycloakGroupManager.processNewUser(user, responseEntity.getBody().getAccessToken(), true); saveUserOnSession(request, user); log.info("Successfully authenticated user {}", user.getUsername()); } catch (HttpClientErrorException e) { diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index 06867deafe..b367774a44 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -1,6 +1,8 @@ package org.entando.entando.keycloak.services; +import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; +import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.CLIENTROLE; import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.GROUP; import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.GROUPROLE; import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.ROLE; @@ -14,13 +16,20 @@ import com.agiletec.aps.system.services.role.Role; import com.agiletec.aps.system.services.role.RoleManager; import com.agiletec.aps.system.services.user.UserDetails; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.apache.commons.lang3.StringUtils; import org.entando.entando.ent.exception.EntException; import org.entando.entando.ent.util.EntLogging.EntLogFactory; @@ -45,6 +54,12 @@ public class KeycloakAuthorizationManager extends AbstractService { private static final int GROUP_POSITION = 0; private static final int ROLE_POSITION = 1; + private final ObjectMapper mapper = new ObjectMapper(); + + private final ReadWriteLock configUpdateLock = new ReentrantReadWriteLock(); + private final Lock readLock = configUpdateLock.readLock(); + private final Lock writeLock = configUpdateLock.writeLock(); + @Autowired public KeycloakAuthorizationManager(final KeycloakConfiguration configuration, final AuthorizationManager authorizationManager, @@ -59,12 +74,15 @@ public KeycloakAuthorizationManager(final KeycloakConfiguration configuration, } /** - * Dynamic mapping elements that are currently activeMappings based on their enabled status. + * Immutable list of mapping elements that are currently active. The configuration is constantly updated + * either by reloading the global configuration or after a certain amount of time (by default, + * one minute) */ private List activeMappings; @Override public void init() throws Exception { + writeLock.lock(); try { String xml = configManager.getConfigItem("dynamicAuthMapping"); if (StringUtils.isNotBlank(xml)) { @@ -76,16 +94,28 @@ public void init() throws Exception { .stream() .filter(this::isValid) .filter(d -> (d.enabled != null && d.enabled)) - .collect(Collectors.toList()); - log.info("{} dynamic auth mapping found, {} activeMappings", + .collect(Collectors.toUnmodifiableList()); + log.debug("{} dynamic auth mapping found, {} activeMappings", dynConf.mapping.size(), activeMappings.size()); - log.info("*** Dynamic mapping will be refreshed after configuration reload ***"); } } + log.info("Dynamic configuration processed: {} active elements found", activeMappings.size()); } catch (Exception e) { log.error("Error initializing KeycloakAuthorizationManager", e); + } finally { + writeLock.unlock(); + } + } + + /** + * This is invoked after a configured time to update the configuration + */ + public void refreshConfiguration() { + try { + init(); + } catch (Exception e) { + log.error("Error refreshing dynamic mapping configuration"); } - log.info("{} init completed", this.getClass()); } /** @@ -94,21 +124,78 @@ public void init() throws Exception { * @return true if the element is valid, false otherwise */ private boolean isValid(DynamicMappingElement elem) { - if (StringUtils.isBlank(elem.attribute)) { - log.error("invalid dynamic mapping element, 'attribute' is blank"); - return false; - } if (elem.kind == null) { log.error("invalid dynamic mapping element, 'kind' is blank"); return false; } - if (elem.kind != ROLE && elem.kind != GROUP && elem.kind != GROUPROLE) { + if (elem.kind != ROLE && elem.kind != GROUP && elem.kind != GROUPROLE && elem.kind != CLIENTROLE) { log.error("invalid dynamic mapping element, kind '{}' is unknown", elem.kind); return false; } + if (StringUtils.isBlank(elem.attribute) && elem.kind != CLIENTROLE) { + log.error("invalid dynamic mapping element, 'attribute' is blank"); + return false; + } + if (StringUtils.isBlank(elem.client) && elem.kind == CLIENTROLE) { + log.error("invalid dynamic mapping element, 'client' is blank for CLIENTROLE kind"); + return false; + } + if (StringUtils.isBlank(elem.separator) && elem.kind == GROUPROLE) { + log.error("invalid dynamic mapping element, 'separator' is blank for GROUPROLE kind"); + return false; + } return true; } + public void processNewUser(final UserDetails user, final String token, boolean decode) { + processNewUser(user); + readLock.lock(); + try { + final DynamicMappingElement tokenMapper = ofNullable(activeMappings) + .orElse(emptyList()) + .stream() + .filter(m -> m.kind == CLIENTROLE) + .findFirst().orElse(null); + // process client claim... + if (StringUtils.isNotBlank(token) && tokenMapper != null) { + final String payload = decode ? token.split("\\.")[1] : token; + final String json = decode ? new String(Base64.getUrlDecoder().decode(payload)) : payload; + + try { + final JsonNode root = mapper.readTree(json); + + JsonNode roleNode = root + .path("resource_access") + .path(tokenMapper.client) + .path("roles"); + + if (roleNode == null) { + return; + } + + List roles = StreamSupport.stream(roleNode.spliterator(), false) + .map(JsonNode::asText) + .collect(Collectors.toList()); + if (user instanceof KeycloakUser) { + finalizeRoleAssociation((KeycloakUser) user, tokenMapper, roles); + } + + } catch (Exception e) { + log.error("error importing client role into Entando roles", e); + } + } + // ...then process attributes coming from the user profile + if (user instanceof KeycloakUser + && activeMappings != null + && !activeMappings.isEmpty()) { + processProfileAttributes((KeycloakUser) user); + } + } finally { + readLock.unlock(); + } + } + + @Deprecated public void processNewUser(final UserDetails user) { if (StringUtils.isNotEmpty(configuration.getDefaultAuthorizations())) { // process group and role coming from the configuration @@ -123,10 +210,6 @@ public void processNewUser(final UserDetails user) { .filter(defaultGroup -> !userAuthorizations.contains(defaultGroup)) .forEach(authorization -> this.assignGroupToUser(authorization, user)); } - // process mapping coming from the user profile - if (user instanceof KeycloakUser) { - processDynamicMapping((KeycloakUser) user); - } } private void assignGroupToUser(final String authorization, final UserDetails user) { @@ -146,7 +229,7 @@ private void assignGroupToUser(final String authorization, final UserDetails use } } - private Group findOrCreateGroup(final String groupName) { + private synchronized Group findOrCreateGroup(final String groupName) { Group group = groupManager.getGroup(groupName); if (group == null) { group = new Group(); @@ -161,7 +244,7 @@ private Group findOrCreateGroup(final String groupName) { return group; } - private Role findOrCreateRole(final String roleName) { + private synchronized Role findOrCreateRole(final String roleName) { Role role = roleManager.getRole(roleName); if (role == null) { role = new Role(); @@ -181,21 +264,18 @@ private Role findOrCreateRole(final String roleName) { * keycloak * @param user the currently logged user */ - private synchronized void processDynamicMapping(final KeycloakUser user) { - if (activeMappings != null && !activeMappings.isEmpty()) { - - activeMappings.forEach(m -> { - if (m.kind == ROLE) { - doProcessRole(user, m); - } - if (m.kind == GROUP) { - doProcessGroup(user, m); - } - if (m.kind == GROUPROLE) { - doProcessGroupRole(user, m); - } - }); - } + private synchronized void processProfileAttributes(final KeycloakUser user) { + activeMappings.forEach(m -> { + if (m.kind == ROLE) { + doProcessRole(user, m); + } + if (m.kind == GROUP) { + doProcessGroup(user, m); + } + if (m.kind == GROUPROLE) { + doProcessGroupRole(user, m); + } + }); } private void doProcessGroupRole(KeycloakUser user, DynamicMappingElement elem) { @@ -203,7 +283,7 @@ private void doProcessGroupRole(KeycloakUser user, DynamicMappingElement elem) { DEFAULT_SEPARATOR : elem.separator; try { - final List authorizations = processDynamicConfiguration(user, elem); + final List authorizations = processUserProfileAttribute(user, elem); if (authorizations == null) { return; @@ -258,7 +338,11 @@ private void doProcessGroupRole(KeycloakUser user, DynamicMappingElement elem) { * @param elem a single dynamic configuration */ private void doProcessRole(KeycloakUser user, DynamicMappingElement elem) { - final List authorizations = processDynamicConfiguration(user, elem); + final List authorizations = processUserProfileAttribute(user, elem); + finalizeRoleAssociation(user, elem, authorizations); + } + + private void finalizeRoleAssociation(KeycloakUser user, DynamicMappingElement elem, List authorizations) { if (authorizations == null) { return; } @@ -286,7 +370,7 @@ private void doProcessRole(KeycloakUser user, DynamicMappingElement elem) { private Authorization createPersistedRoleAuthorization(KeycloakUser user, String roleName) throws EntException { Role role = findOrCreateRole(roleName); - Authorization auth= new Authorization(null, role); + Authorization auth = new Authorization(null, role); persistAuthIfMissing(user, auth); return auth; } @@ -306,7 +390,7 @@ private Authorization createTransientRoleAuthorization(String roleName) { * @param elem a single dynamic configuration */ private void doProcessGroup(KeycloakUser user, DynamicMappingElement elem) { - final List authorizations = processDynamicConfiguration(user, elem); + final List authorizations = processUserProfileAttribute(user, elem); if (authorizations == null) { return; } @@ -351,7 +435,7 @@ private Authorization createTransientGroupAuthorization(String groupName) { * @param elem the dynamic mapping element * @return the list of processed attribute tokens or null if the attribute is missing */ - private static List processDynamicConfiguration(KeycloakUser user, DynamicMappingElement elem) { + private static List processUserProfileAttribute(KeycloakUser user, DynamicMappingElement elem) { if (user.getUserRepresentation() == null || user.getUserRepresentation().getAttributes() == null || !user.getUserRepresentation().getAttributes().containsKey(elem.attribute)) { @@ -370,7 +454,7 @@ private static List processDynamicConfiguration(KeycloakUser user, Dynam * @param auth the authorization to persist * @throws EntException in case of errors */ - private void persistAuthIfMissing(KeycloakUser user, Authorization auth) throws EntException { + private synchronized void persistAuthIfMissing(KeycloakUser user, Authorization auth) throws EntException { final List existing = authorizationManager.getUserAuthorizations(user.getUsername()); if (existing.stream() .noneMatch(a -> (a.getGroup() != null && a.getRole() == null @@ -389,7 +473,7 @@ private void persistAuthIfMissing(KeycloakUser user, Authorization auth) throws && a.getGroup().getName().equals(auth.getGroup().getName())) ) ) { - log.info("dynamically persisting authorization for user '{}' : group {}, role {}", user.getUsername(), + log.error("dynamically persisting authorization for user '{}' : group {}, role {}", user.getUsername(), auth.getGroup() != null ? auth.getGroup().getName() : "N/A", auth.getRole() != null ? auth.getRole().getName() : "N/A"); authorizationManager.addUserAuthorization(user.getUsername(), auth); diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java index dd54e062d3..2a56525753 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java @@ -8,7 +8,8 @@ public class DynamicMappingElement { public DynamicMappingKind kind; public String injectTo; public Boolean persist; - public String separator; + public String separator; // FOR GROUPROLE ONLY + public String client; // FOR CLIENTROLE ONLY public String toString() { return "DynamicMappingElement(enabled=" + this.enabled + ", attribute=" + this.attribute + ", kind=" diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java index 5a1206397e..11ed339be2 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java @@ -8,7 +8,8 @@ public enum DynamicMappingKind { GROUP("group"), ROLE("role"), - GROUPROLE("grouprole"); + GROUPROLE("grouprole"), + CLIENTROLE("clientrole"); public String kind; DynamicMappingKind(String kind) { diff --git a/keycloak-plugin/src/main/resources/spring/plugins/keycloak/aps/keycloak.xml b/keycloak-plugin/src/main/resources/spring/plugins/keycloak/aps/keycloak.xml index 9dc5815aed..dc9f3a1a57 100644 --- a/keycloak-plugin/src/main/resources/spring/plugins/keycloak/aps/keycloak.xml +++ b/keycloak-plugin/src/main/resources/spring/plugins/keycloak/aps/keycloak.xml @@ -1,12 +1,19 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:context="http://www.springframework.org/schema/context" + xmlns:task="http://www.springframework.org/schema/task" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd"> + + + diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java index faa44f89b1..777a94a829 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java @@ -7,6 +7,7 @@ import com.agiletec.aps.system.services.group.GroupManager; import com.agiletec.aps.system.services.role.Role; import com.agiletec.aps.system.services.role.RoleManager; +import com.agiletec.aps.system.services.user.UserDetails; import java.util.List; import java.util.Map; import org.entando.entando.keycloak.services.oidc.model.KeycloakUser; @@ -42,6 +43,7 @@ class KeycloakAuthorizationManagerTest { @Mock private GroupManager groupManager; @Mock private RoleManager roleManager; @Mock private BaseConfigManager configManager; + @Mock private KeycloakUserManager userManager; private KeycloakAuthorizationManager manager; @@ -128,29 +130,50 @@ void testVerification() { @Test void testDynamicConfigurationRoleOnLogin() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); -// when(groupManager.getGroup(anyString())).thenReturn(null); when(roleManager.getRole(anyString())).thenReturn(null); when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + when(userDetails.getUsername()).thenReturn("testuser"); when(configManager.getConfigItem(anyString())).thenReturn(XML_ROLE_CONF); UserRepresentation userRepresentation = new UserRepresentation(); userRepresentation.setAttributes(Map.of("AD_ROLE", List.of("ruolo"))); - when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); - + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); manager.init(); final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); - manager.processNewUser(userDetails); + manager.processNewUser(userDetails, JWT, false); - verify(authorizationManager, times(1)).addUserAuthorization(isNull(), authCaptor.capture()); + verify(authorizationManager, times(1)).addUserAuthorization(eq("testuser"), authCaptor.capture()); assertThat(authCaptor.getValue().getRole().getName()).isEqualTo("ruolo"); assertThat(authCaptor.getValue().getGroup()).isNull(); } + @Test + void testDynamicConfigurationRoleOnLoginFromJwt() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(roleManager.getRole(anyString())).thenReturn(null); + when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + when(userDetails.getUsername()).thenReturn("testuser"); + when(configManager.getConfigItem(anyString())).thenReturn(XML_CLIENT_ROLE); + + UserRepresentation userRepresentation = new UserRepresentation(); + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails, JWT, false); + + verify(authorizationManager, times(1)).addUserAuthorization(eq("testuser"), authCaptor.capture()); + + assertThat(authCaptor.getValue().getRole().getName()).isEqualTo("generico"); + assertThat(authCaptor.getValue().getGroup()).isNull(); + } + @Test void testDynamicConfigurationGroupOnLogin() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); @@ -159,15 +182,16 @@ void testDynamicConfigurationGroupOnLogin() throws Exception { UserRepresentation userRepresentation = new UserRepresentation(); userRepresentation.setAttributes(Map.of("AD_GROUP", List.of("group"))); - when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); manager.init(); final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); - manager.processNewUser(userDetails); + manager.processNewUser(userDetails, JWT, false); - verify(authorizationManager, times(1)).addUserAuthorization(isNull(), authCaptor.capture()); + verify(authorizationManager, times(1)).addUserAuthorization(eq("testuser"), authCaptor.capture()); assertThat(authCaptor.getValue().getGroup().getName()).isEqualTo("group"); assertThat(authCaptor.getValue().getRole()).isNull(); @@ -181,15 +205,16 @@ void testDynamicConfigurationGroupRoleOnLogin() throws Exception { UserRepresentation userRepresentation = new UserRepresentation(); userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("agroup_r_arole"))); - when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); manager.init(); final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); - manager.processNewUser(userDetails); + manager.processNewUser(userDetails, JWT, false); - verify(authorizationManager, times(1)).addUserAuthorization(isNull(), authCaptor.capture()); + verify(authorizationManager, times(1)).addUserAuthorization(eq("testuser"), authCaptor.capture()); assertThat(authCaptor.getValue().getGroup().getName()).isEqualTo("agroup"); assertThat(authCaptor.getValue().getRole().getName()).isEqualTo("arole"); @@ -203,15 +228,15 @@ void testDynamicConfigurationNoGroupOnlyRoleOnLogin() throws Exception { UserRepresentation userRepresentation = new UserRepresentation(); userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("_r_arole"))); - when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); manager.init(); final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); - manager.processNewUser(userDetails); + manager.processNewUser(userDetails, JWT, false); - verify(authorizationManager, never()).addUserAuthorization(isNull(), any()); + verify(authorizationManager, never()).addUserAuthorization(anyString(), any()); } @Test @@ -222,15 +247,13 @@ void testDynamicConfigurationOnlyGroupNoRoleOnLogin() throws Exception { UserRepresentation userRepresentation = new UserRepresentation(); userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("group_r_"))); - when((userDetails).getUserRepresentation()).thenReturn(userRepresentation); + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); manager.init(); - final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + manager.processNewUser(userDetails, JWT, false); - manager.processNewUser(userDetails); - - verify(authorizationManager, never()).addUserAuthorization(isNull(), any()); + verify(authorizationManager, never()).addUserAuthorization(anyString(), any()); } @@ -307,4 +330,93 @@ private Authorization authorization(final String groupName, final String roleNam + " true" + " " + ""; + + private static final String XML_CLIENT_ROLE = "" + + " " + + " true" + + " sim730" + + " CLIENTROLE" + + " true" + + " " + + ""; + + private static final String _JWT = "{\n" + + " \"header\" : {\n" + + " \"alg\" : \"RS256\",\n" + + " \"typ\" : \"JWT\",\n" + + " \"kid\" : \"l09Wlf_NY_dmMORYBjkr7deFVGVJ5TRLHW1p7DIT1ds\"\n" + + " },\n" + + " \"payload\" : {\n" + + " \"exp\" : 1768319443,\n" + + " \"iat\" : 1768319143,\n" + + " \"auth_time\" : 1768319142,\n" + + " \"jti\" : \"e64ed1da-aa8c-488f-be10-09e0c2f580c3\",\n" + + " \"iss\" : \"https://localhost:8080/auth/realms/entando\",\n" + + " \"aud\" : [ \"sim730\", \"account\" ],\n" + + " \"sub\" : \"5e7213c6-ad81-4094-bb24-fead709b05af\",\n" + + " \"typ\" : \"Bearer\",\n" + + " \"azp\" : \"entando-web\",\n" + + " \"nonce\" : \"6a9f89c2-c904-4e9e-80cb-e8c1ccddd1e0\",\n" + + " \"session_state\" : \"0503e261-d522-41b6-8096-1debdd2c86e7\",\n" + + " \"acr\" : \"1\",\n" + + " \"allowed-origins\" : [ \"https://localhost:8080\", \"*\" ],\n" + + " \"realm_access\" : {\n" + + " \"roles\" : [ \"offline_access\", \"uma_authorization\", \"default-roles-entando\" ]\n" + + " },\n" + + " \"resource_access\" : {\n" + + " \"sim730\" : {\n" + + " \"roles\" : [ \"generico\" ]\n" + + " },\n" + + " \"account\" : {\n" + + " \"roles\" : [ \"manage-account\", \"manage-account-links\", \"view-profile\" ]\n" + + " }\n" + + " },\n" + + " \"scope\" : \"openid profile email\",\n" + + " \"sid\" : \"0503e261-d522-41b6-8096-1debdd2c86e7\",\n" + + " \"email_verified\" : false,\n" + + " \"name\" : \"User lastname\",\n" + + " \"preferred_username\" : \"user@email.it\",\n" + + " \"given_name\" : \"User\",\n" + + " \"family_name\" : \"lastname\",\n" + + " \"email\" : \"user@email.it\",\n" + + " \"miei_ruoli_custom\" : [ \"offline_access\", \"uma_authorization\", \"default-roles-entando\" ]\n" + + " },\n" + + " \"signature\" : \"dLENSPEPw\"\n" + + "}"; + + private static final String JWT = "{\n" + + " \"exp\" : 1768319443,\n" + + " \"iat\" : 1768319143,\n" + + " \"auth_time\" : 1768319142,\n" + + " \"jti\" : \"e64ed1da-aa8c-488f-be10-09e0c2f580c3\",\n" + + " \"iss\" : \"https://localhost:8080/auth/realms/entando\",\n" + + " \"aud\" : [ \"sim730\", \"account\" ],\n" + + " \"sub\" : \"5e7213c6-ad81-4094-bb24-fead709b05af\",\n" + + " \"typ\" : \"Bearer\",\n" + + " \"azp\" : \"entando-web\",\n" + + " \"nonce\" : \"6a9f89c2-c904-4e9e-80cb-e8c1ccddd1e0\",\n" + + " \"session_state\" : \"0503e261-d522-41b6-8096-1debdd2c86e7\",\n" + + " \"acr\" : \"1\",\n" + + " \"allowed-origins\" : [ \"https://localhost:8080\", \"*\" ],\n" + + " \"realm_access\" : {\n" + + " \"roles\" : [ \"offline_access\", \"uma_authorization\", \"default-roles-entando\" ]\n" + + " },\n" + + " \"resource_access\" : {\n" + + " \"sim730\" : {\n" + + " \"roles\" : [ \"generico\" ]\n" + + " },\n" + + " \"account\" : {\n" + + " \"roles\" : [ \"manage-account\", \"manage-account-links\", \"view-profile\" ]\n" + + " }\n" + + " },\n" + + " \"scope\" : \"openid profile email\",\n" + + " \"sid\" : \"0503e261-d522-41b6-8096-1debdd2c86e7\",\n" + + " \"email_verified\" : false,\n" + + " \"name\" : \"User lastname\",\n" + + " \"preferred_username\" : \"user@email.it\",\n" + + " \"given_name\" : \"User\",\n" + + " \"family_name\" : \"lastname\",\n" + + " \"email\" : \"user@email.it\",\n" + + " \"miei_ruoli_custom\" : [ \"offline_access\", \"uma_authorization\", \"default-roles-entando\" ]\n" + + " }"; } From f3fbdd8f4095b6091cf57678302c52904d35f022 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Wed, 14 Jan 2026 09:44:04 +0100 Subject: [PATCH 05/15] ESB-890: minor refacor --- .../keycloak/services/KeycloakService.java | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java index 06ee040dbc..c9009abe30 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java @@ -66,29 +66,26 @@ public List listUsers(String text) { List retval = null; String token = this.extractToken(); - try { - if (!isExact && !params.isEmpty()) { - params.put("briefRepresentation", "true"); - params.put("search", searchString); -// params.put("first", "0"); -// params.put("max", "20"); - params.remove("username"); - } - final ResponseEntity response = this.executeEscapedRequest(token, url, - HttpMethod.GET, createEntity(token, null), UserRepresentation[].class, params, 0); - retval = Optional.ofNullable(response.getBody()) - .map(Arrays::asList) - .orElse(Collections.emptyList()); - if (retval.size() > 1) { - // must match the exact element by USERNAME - Optional userOpt = retval.stream() - .filter(e -> (e.getUsername() != null && e.getUsername().equals(text)) - || (e.getEmail() != null && e.getEmail().equals(text))) - .findFirst(); - return userOpt.stream().collect(Collectors.toList()); - } - } catch (Exception e) { - log.error("Error listing users from Keycloak: {}", e.getMessage(), e); + if (!isExact && !params.isEmpty()) { + params.put("briefRepresentation", "true"); + params.put("search", searchString); + // reference for paged request: + // params.put("first", "0"); + // params.put("max", "20"); + params.remove("username"); + } + final ResponseEntity response = this.executeEscapedRequest(token, url, + HttpMethod.GET, createEntity(token, null), UserRepresentation[].class, params, 0); + retval = Optional.ofNullable(response.getBody()) + .map(Arrays::asList) + .orElse(Collections.emptyList()); + if (retval.size() > 1) { + // must match the exact element by USERNAME + Optional userOpt = retval.stream() + .filter(e -> (e.getUsername() != null && e.getUsername().equals(text)) + || (e.getEmail() != null && e.getEmail().equals(text))) + .findFirst(); + return userOpt.stream().collect(Collectors.toList()); } return retval; } @@ -158,12 +155,12 @@ private ResponseEntity executeRequest(String token, final String url, } private ResponseEntity executeRequest(String token, final String url, final HttpMethod method, final HttpEntity entity, - final Class result, final Map params) { + final Class result, final Map params) { return executeRequest(token, url, method, entity, result, params, 0); } private ResponseEntity executeRequest(String token, final String url, final HttpMethod method, final HttpEntity entity, - final Class result, final Map params, int retryCount) { + final Class result, final Map params, int retryCount) { try { final UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url); params.forEach(builder::queryParam); From f67ce32cd426925c2f831d9b3ec7175fcd7b3243 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Wed, 14 Jan 2026 13:02:52 +0100 Subject: [PATCH 06/15] ESB-890: code cleaning --- .../entity/AbstractEntitySearcherDAO.java | 2 - .../authorization/AuthorizationManager.java | 4 +- .../KeycloakAuthorizationManager.java | 13 +- .../keycloak/services/KeycloakService.java | 13 +- .../services/mapping/DynamicMapping.java | 2 + .../KeycloakAuthorizationManagerTest.java | 146 +++++++++++++++--- 6 files changed, 139 insertions(+), 41 deletions(-) diff --git a/engine/src/main/java/com/agiletec/aps/system/common/entity/AbstractEntitySearcherDAO.java b/engine/src/main/java/com/agiletec/aps/system/common/entity/AbstractEntitySearcherDAO.java index 06fa38e86e..4353296769 100644 --- a/engine/src/main/java/com/agiletec/aps/system/common/entity/AbstractEntitySearcherDAO.java +++ b/engine/src/main/java/com/agiletec/aps/system/common/entity/AbstractEntitySearcherDAO.java @@ -137,8 +137,6 @@ protected EntitySearchFilter[] addFilter(EntitySearchFilter[] filters, EntitySea private PreparedStatement buildStatement(EntitySearchFilter[] filters, boolean isCount, boolean selectAll, Connection conn) { String query = this.createQueryString(filters, isCount, selectAll); - System.out.println("\n>>> " + query); - PreparedStatement stat = null; try { stat = conn.prepareStatement(query); diff --git a/engine/src/main/java/com/agiletec/aps/system/services/authorization/AuthorizationManager.java b/engine/src/main/java/com/agiletec/aps/system/services/authorization/AuthorizationManager.java index 2fa04470e5..cf868a58bc 100644 --- a/engine/src/main/java/com/agiletec/aps/system/services/authorization/AuthorizationManager.java +++ b/engine/src/main/java/com/agiletec/aps/system/services/authorization/AuthorizationManager.java @@ -40,7 +40,7 @@ /** * Servizio di autorizzazione. Il servizio espone tutti i metodi necessari per - * la verifica verifica delle autorizzazioni utente, qualsiasi sia la sua + * la verifica delle autorizzazioni utente, qualsiasi sia la sua * provenienza e definizione. * @author E.Santoboni */ @@ -749,4 +749,4 @@ public void setGroupManager(IGroupManager groupManager) { private IRoleManager _roleManager; private IGroupManager _groupManager; -} \ No newline at end of file +} diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index b367774a44..1217c05ce8 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -55,6 +55,7 @@ public class KeycloakAuthorizationManager extends AbstractService { private static final int ROLE_POSITION = 1; private final ObjectMapper mapper = new ObjectMapper(); + private final XmlMapper xmlMapper = new XmlMapper(); private final ReadWriteLock configUpdateLock = new ReentrantReadWriteLock(); private final Lock readLock = configUpdateLock.readLock(); @@ -86,8 +87,7 @@ public void init() throws Exception { try { String xml = configManager.getConfigItem("dynamicAuthMapping"); if (StringUtils.isNotBlank(xml)) { - XmlMapper mapper = new XmlMapper(); - DynamicMapping dynConf = mapper.readValue(xml, DynamicMapping.class); + DynamicMapping dynConf = xmlMapper.readValue(xml, DynamicMapping.class); if (dynConf != null && dynConf.mapping != null) { activeMappings = dynConf.mapping @@ -99,9 +99,12 @@ public void init() throws Exception { dynConf.mapping.size(), activeMappings.size()); } } - log.info("Dynamic configuration processed: {} active elements found", activeMappings.size()); + if (activeMappings != null) { + log.info("Dynamic configuration processed: {} active elements found", activeMappings.size()); + } } catch (Exception e) { log.error("Error initializing KeycloakAuthorizationManager", e); + throw e; } finally { writeLock.unlock(); } @@ -195,7 +198,6 @@ public void processNewUser(final UserDetails user, final String token, boolean d } } - @Deprecated public void processNewUser(final UserDetails user) { if (StringUtils.isNotEmpty(configuration.getDefaultAuthorizations())) { // process group and role coming from the configuration @@ -313,7 +315,6 @@ private void doProcessGroupRole(KeycloakUser user, DynamicMappingElement elem) { persistAuthIfMissing(user, authorization); } else { - if (StringUtils.isNotBlank(groupName)) { group = new Group(); group.setName(groupName); @@ -473,7 +474,7 @@ private synchronized void persistAuthIfMissing(KeycloakUser user, Authorization && a.getGroup().getName().equals(auth.getGroup().getName())) ) ) { - log.error("dynamically persisting authorization for user '{}' : group {}, role {}", user.getUsername(), + log.debug("dynamically persisting authorization for user '{}' : group {}, role {}", user.getUsername(), auth.getGroup() != null ? auth.getGroup().getName() : "N/A", auth.getRole() != null ? auth.getRole().getName() : "N/A"); authorizationManager.addUserAuthorization(user.getUsername(), auth); diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java index c9009abe30..e21921c12a 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java @@ -54,17 +54,14 @@ public List listUsers() { } // Handle invocations such as http://localhost:8081/auth/admin/realms/entando-development/users?briefRepresentation=true&first=0&max=20&search=testutentemariorossi%2B4375@gmail.com - public List listUsers(String text) { + public List listUsers(final String text) { final String url = String.format("%s/admin/realms/%s/users", configuration.getAuthUrl(), configuration.getRealm()); final String searchString = StringUtils.isNotBlank(text) ? encodeForKeycloakSearchAPI(text) : text; final boolean isExact = StringUtils.isBlank(text) || (StringUtils.isNotBlank(text) && searchString.equals(text)); - final Map params = StringUtils.isBlank(text) - ? Collections.emptyMap() - : new HashMap<>() {{ - put("username", searchString); - }}; + final Map params = StringUtils.isBlank(text) ? Collections.emptyMap() + : Map.of("username", searchString); List retval = null; - String token = this.extractToken(); + final String token = this.extractToken(); if (!isExact && !params.isEmpty()) { params.put("briefRepresentation", "true"); @@ -205,8 +202,6 @@ private ResponseEntity executeEscapedRequest(String token, final Strin queryBuilder.append(key).append("=").append(value); }); String escapedUrl = builder.build().toUri() + "?" + queryBuilder; - -// restTemplate.setInterceptors(Collections.singletonList(new LoggingInterceptor())); ResponseEntity retval = restTemplate.exchange( URI.create(escapedUrl), method, createEntity(token, entity.getBody()), result); diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java index 6606f7296a..b3b30ecdd5 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java @@ -1,10 +1,12 @@ package org.entando.entando.keycloak.services.mapping; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import java.util.List; @JacksonXmlRootElement(localName = "dynamicmapping") +@JsonInclude(JsonInclude.Include.NON_EMPTY) public class DynamicMapping { @JacksonXmlElementWrapper(useWrapping = false) diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java index 777a94a829..9c0fa33d4d 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java @@ -1,5 +1,15 @@ package org.entando.entando.keycloak.services; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.agiletec.aps.system.services.authorization.Authorization; import com.agiletec.aps.system.services.authorization.AuthorizationManager; import com.agiletec.aps.system.services.baseconfig.BaseConfigManager; @@ -7,31 +17,19 @@ import com.agiletec.aps.system.services.group.GroupManager; import com.agiletec.aps.system.services.role.Role; import com.agiletec.aps.system.services.role.RoleManager; -import com.agiletec.aps.system.services.user.UserDetails; +import com.fasterxml.jackson.core.JsonParseException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import org.entando.entando.ent.exception.EntException; import org.entando.entando.keycloak.services.oidc.model.KeycloakUser; import org.entando.entando.keycloak.services.oidc.model.UserRepresentation; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; - -import java.util.ArrayList; -import java.util.Arrays; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.entando.entando.ent.exception.EntException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -174,6 +172,22 @@ void testDynamicConfigurationRoleOnLoginFromJwt() throws Exception { assertThat(authCaptor.getValue().getGroup()).isNull(); } + @Test + void testDynamicConfigurationRoleOnLoginWithWrongJwt() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_CLIENT_ROLE); + + UserRepresentation userRepresentation = new UserRepresentation(); + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails, JWT_NO_ROLE, false); + + verify(authorizationManager, never()).addUserAuthorization(eq("testuser"), authCaptor.capture()); + } + @Test void testDynamicConfigurationGroupOnLogin() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); @@ -256,6 +270,50 @@ void testDynamicConfigurationOnlyGroupNoRoleOnLogin() throws Exception { verify(authorizationManager, never()).addUserAuthorization(anyString(), any()); } + @Test + void testDynamicConfigurationNoMapping() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_NO_MAPPING); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("group_r_"))); + + manager.init(); + + manager.processNewUser(userDetails, JWT, false); + + verify(authorizationManager, never()).addUserAuthorization(anyString(), any()); + } + + @Test + void testDynamicConfigurationMalformedMapping() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_MALFORMED_MAPPING); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("group_r_"))); + + assertThrows(JsonParseException.class, () -> manager.init()); + + manager.processNewUser(userDetails, JWT, false); + + verify(authorizationManager, never()).addUserAuthorization(anyString(), any()); + } + + @Test + void testDynamicConfigurationWrongMapping() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_WRONG_CONF); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("group_r_"))); + + manager.init(); + + manager.processNewUser(userDetails, JWT, false); + + verify(authorizationManager, never()).addUserAuthorization(anyString(), any()); + } private Authorization authorization(final String groupName, final String roleName) { final Group group = new Group(); @@ -283,7 +341,7 @@ private Authorization authorization(final String groupName, final String roleNam + " AD_GROUPROLE" + " GROUPROLE" + " _r_" - + " true " + + " true" + " " + ""; @@ -305,7 +363,7 @@ private Authorization authorization(final String groupName, final String roleNam + " AD_GROUPROLE" + " GROUPROLE" + " _r_" - + " true " + + " true" + " " + ""; @@ -339,8 +397,52 @@ private Authorization authorization(final String groupName, final String roleNam + " true" + " " + ""; + + private static final String XML_NO_MAPPING = "" + + ""; + + private static final String XML_MALFORMED_MAPPING = "" + + "" + + " false" + + " AD_ROLE" +// + " ROLE" // kind null + + " true" + + " " + + " " + + " true" + + " AD_GROUP" + + " GROUP" // unknown + + " true" + + " " + + " " + + " false" +// + " AD_GROUPROLE" // attribute null + + " GROUPROLE" + + " _r_" + + " true" + + " " + + + " " + + " false" + + " AD_ROLE" + + " CLIENTROLE" // no client + + " true" + + " " + + + " " + + " false" + + " AD_GROUPROLE" + + " GROUPROLE" +// + " _r_" // separator null + + " true" + + " " + + + ""; - private static final String _JWT = "{\n" + private static final String JWT_NO_ROLE = "{\n" + " \"header\" : {\n" + " \"alg\" : \"RS256\",\n" + " \"typ\" : \"JWT\",\n" @@ -364,7 +466,7 @@ private Authorization authorization(final String groupName, final String roleNam + " \"roles\" : [ \"offline_access\", \"uma_authorization\", \"default-roles-entando\" ]\n" + " },\n" + " \"resource_access\" : {\n" - + " \"sim730\" : {\n" + + " \"aclient\" : {\n" + " \"roles\" : [ \"generico\" ]\n" + " },\n" + " \"account\" : {\n" From 9e06ae0161aa5af45e0710e545aaabbb56cd34f3 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Wed, 14 Jan 2026 14:54:07 +0100 Subject: [PATCH 07/15] ESB-890: code cleaning --- .../KeycloakAuthorizationManager.java | 4 - .../keycloak/services/KeycloakService.java | 10 +- .../KeycloakAuthorizationManagerTest.java | 102 ++++++++++++++++-- 3 files changed, 99 insertions(+), 17 deletions(-) diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index 1217c05ce8..6d14d30f32 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -131,10 +131,6 @@ private boolean isValid(DynamicMappingElement elem) { log.error("invalid dynamic mapping element, 'kind' is blank"); return false; } - if (elem.kind != ROLE && elem.kind != GROUP && elem.kind != GROUPROLE && elem.kind != CLIENTROLE) { - log.error("invalid dynamic mapping element, kind '{}' is unknown", elem.kind); - return false; - } if (StringUtils.isBlank(elem.attribute) && elem.kind != CLIENTROLE) { log.error("invalid dynamic mapping element, 'attribute' is blank"); return false; diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java index e21921c12a..3453eeaf80 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java @@ -58,9 +58,13 @@ public List listUsers(final String text) { final String url = String.format("%s/admin/realms/%s/users", configuration.getAuthUrl(), configuration.getRealm()); final String searchString = StringUtils.isNotBlank(text) ? encodeForKeycloakSearchAPI(text) : text; final boolean isExact = StringUtils.isBlank(text) || (StringUtils.isNotBlank(text) && searchString.equals(text)); - final Map params = StringUtils.isBlank(text) ? Collections.emptyMap() - : Map.of("username", searchString); - List retval = null; + final Map params = new HashMap<>(); + + if (StringUtils.isNotBlank(text)) { + params.put("username", searchString); + } + + List retval; final String token = this.extractToken(); if (!isExact && !params.isEmpty()) { diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java index 9c0fa33d4d..576fc1c429 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java @@ -1,6 +1,7 @@ package org.entando.entando.keycloak.services; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -211,6 +212,29 @@ void testDynamicConfigurationGroupOnLogin() throws Exception { assertThat(authCaptor.getValue().getRole()).isNull(); } + @Test + void testDynamicConfigurationGroupOnLoginNoPersist() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_GROUP_CONF_NO_PERSIST); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUP", List.of("group"))); + + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.init(); + manager.processNewUser(userDetails, JWT, false); + + verify(authorizationManager, never()).addUserAuthorization(eq("testuser"), any()); + verify(userDetails, times(1)).addAuthorization(authCaptor.capture()); + + assertThat(authCaptor.getValue().getGroup().getName()).isEqualTo("group"); + assertThat(authCaptor.getValue().getRole()).isNull(); + } + @Test void testDynamicConfigurationGroupRoleOnLogin() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); @@ -234,6 +258,30 @@ void testDynamicConfigurationGroupRoleOnLogin() throws Exception { assertThat(authCaptor.getValue().getRole().getName()).isEqualTo("arole"); } + @Test + void testDynamicConfigurationGroupRoleOnLoginNoPersist() throws Exception { + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_GROUP_ROLE_CONF_NO_PERSIST); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("agroup_r_arole"))); + + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails, JWT, false); + + verify(authorizationManager, never()).addUserAuthorization(eq("testuser"), any()); + verify(userDetails, times(1)).addAuthorization(authCaptor.capture()); + + assertThat(authCaptor.getValue().getGroup().getName()).isEqualTo("agroup"); + // role doesn't exist, so it's not associated + assertNull(authCaptor.getValue().getRole()); + } + @Test void testDynamicConfigurationNoGroupOnlyRoleOnLogin() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); @@ -275,9 +323,6 @@ void testDynamicConfigurationNoMapping() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); when(configManager.getConfigItem(anyString())).thenReturn(XML_NO_MAPPING); - UserRepresentation userRepresentation = new UserRepresentation(); - userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("group_r_"))); - manager.init(); manager.processNewUser(userDetails, JWT, false); @@ -290,9 +335,6 @@ void testDynamicConfigurationMalformedMapping() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); when(configManager.getConfigItem(anyString())).thenReturn(XML_MALFORMED_MAPPING); - UserRepresentation userRepresentation = new UserRepresentation(); - userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("group_r_"))); - assertThrows(JsonParseException.class, () -> manager.init()); manager.processNewUser(userDetails, JWT, false); @@ -305,11 +347,7 @@ void testDynamicConfigurationWrongMapping() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); when(configManager.getConfigItem(anyString())).thenReturn(XML_WRONG_CONF); - UserRepresentation userRepresentation = new UserRepresentation(); - userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("group_r_"))); - manager.init(); - manager.processNewUser(userDetails, JWT, false); verify(authorizationManager, never()).addUserAuthorization(anyString(), any()); @@ -367,6 +405,28 @@ private Authorization authorization(final String groupName, final String roleNam + " " + ""; + private static final String XML_GROUP_CONF_NO_PERSIST = "" + + " " + + " false" + + " AD_ROLE" + + " ROLE" + + " true" + + " " + + " " + + " true" + + " AD_GROUP" + + " GROUP" + + " false" + + " " + + " " + + " false" + + " AD_GROUPROLE" + + " GROUPROLE" + + " _r_" + + " true" + + " " + + ""; + private static final String XML_GROUP_ROLE_CONF = "" + " " + " false" @@ -389,6 +449,28 @@ private Authorization authorization(final String groupName, final String roleNam + " " + ""; + private static final String XML_GROUP_ROLE_CONF_NO_PERSIST = "" + + " " + + " false" + + " AD_ROLE" + + " ROLE" + + " true" + + " " + + " " + + " false" + + " AD_GROUP" + + " GROUP" + + " true" + + " " + + " " + + " true" + + " AD_GROUPROLE" + + " GROUPROLE" + + " _r_" + + " false" + + " " + + ""; + private static final String XML_CLIENT_ROLE = "" + " " + " true" From 8d0d90aaa7d6430c4f7b22de081eeb612df4306e Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Wed, 14 Jan 2026 18:43:13 +0100 Subject: [PATCH 08/15] ESB-890: code cleaning --- .../KeycloakAuthorizationManager.java | 87 ++++++++++--------- .../liquibase/keycloak/changeSetPort.xml | 10 +++ .../00000000000003_dataPort_production.xml | 28 ++++++ .../port/clob/production/sysconfig_kc.xml | 28 ++++++ .../KeycloakAuthorizationManagerTest.java | 30 +++++++ 5 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 keycloak-plugin/src/main/resources/liquibase/keycloak/changeSetPort.xml create mode 100644 keycloak-plugin/src/main/resources/liquibase/keycloak/port/00000000000003_dataPort_production.xml create mode 100644 keycloak-plugin/src/main/resources/liquibase/keycloak/port/clob/production/sysconfig_kc.xml diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index 6d14d30f32..88c8174d72 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -57,9 +57,9 @@ public class KeycloakAuthorizationManager extends AbstractService { private final ObjectMapper mapper = new ObjectMapper(); private final XmlMapper xmlMapper = new XmlMapper(); - private final ReadWriteLock configUpdateLock = new ReentrantReadWriteLock(); - private final Lock readLock = configUpdateLock.readLock(); - private final Lock writeLock = configUpdateLock.writeLock(); + private final transient ReadWriteLock configUpdateLock = new ReentrantReadWriteLock(); + private final transient Lock readLock = configUpdateLock.readLock(); + private final transient Lock writeLock = configUpdateLock.writeLock(); @Autowired public KeycloakAuthorizationManager(final KeycloakConfiguration configuration, @@ -79,7 +79,7 @@ public KeycloakAuthorizationManager(final KeycloakConfiguration configuration, * either by reloading the global configuration or after a certain amount of time (by default, * one minute) */ - private List activeMappings; + private transient List activeMappings; @Override public void init() throws Exception { @@ -287,46 +287,55 @@ private void doProcessGroupRole(KeycloakUser user, DynamicMappingElement elem) { return; } for (String groupRoleToken : authorizations) { - final String[] tokens = groupRoleToken.split(separator); - if (tokens.length != 2 || StringUtils.isBlank(tokens[0]) || StringUtils.isBlank(tokens[1])) { - log.error("invalid dynamic config configuration detected"); - return; - } - final String groupName = tokens[0]; - final String roleName = tokens[1]; + parseAuthForGroupRole(user, elem, groupRoleToken, separator); + } + } catch (Exception e) { + log.error("error processing dynamic GRUOPROLE association", e); + } + } - Authorization authorization; - Group group = null; - Role role = null; + private void parseAuthForGroupRole(KeycloakUser user, DynamicMappingElement elem, String groupRoleToken, String separator) + throws EntException { + final String[] tokens = groupRoleToken.split(separator); - if (elem.persist) { - if (StringUtils.isNotBlank(groupName)) { - group = findOrCreateGroup(groupName); - } + if (tokens.length != 2 + || StringUtils.isBlank(tokens[0]) + || StringUtils.isBlank(tokens[1])) { + log.error("invalid dynamic config configuration detected"); + return; + } - if (StringUtils.isNotBlank(roleName)) { - role = findOrCreateRole(roleName); - } - authorization = new Authorization(group, role); - - persistAuthIfMissing(user, authorization); - } else { - if (StringUtils.isNotBlank(groupName)) { - group = new Group(); - group.setName(groupName); - group.setDescription("sys:" + groupName); - } - if (StringUtils.isNotBlank(roleName)) { - // make sure all the permissions are assigned to the current role - role = roleManager.getRole(roleName); - } - authorization = new Authorization(group, role); - } - user.addAuthorization(authorization); + final String groupName = tokens[0]; + final String roleName = tokens[1]; + + Authorization authorization; + Group group = null; + Role role = null; + + if (elem.persist) { + if (StringUtils.isNotBlank(groupName)) { + group = findOrCreateGroup(groupName); } - } catch (Exception e) { - log.error("error processing dynamic GRUOPROLE association", e); + + if (StringUtils.isNotBlank(roleName)) { + role = findOrCreateRole(roleName); + } + authorization = new Authorization(group, role); + + persistAuthIfMissing(user, authorization); + } else { + if (StringUtils.isNotBlank(groupName)) { + group = new Group(); + group.setName(groupName); + group.setDescription("sys:" + groupName); + } + if (StringUtils.isNotBlank(roleName)) { + // make sure all the permissions are assigned to the current role + role = roleManager.getRole(roleName); + } + authorization = new Authorization(group, role); } + user.addAuthorization(authorization); } /** diff --git a/keycloak-plugin/src/main/resources/liquibase/keycloak/changeSetPort.xml b/keycloak-plugin/src/main/resources/liquibase/keycloak/changeSetPort.xml new file mode 100644 index 0000000000..02b9537b7b --- /dev/null +++ b/keycloak-plugin/src/main/resources/liquibase/keycloak/changeSetPort.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/keycloak-plugin/src/main/resources/liquibase/keycloak/port/00000000000003_dataPort_production.xml b/keycloak-plugin/src/main/resources/liquibase/keycloak/port/00000000000003_dataPort_production.xml new file mode 100644 index 0000000000..a0c7732156 --- /dev/null +++ b/keycloak-plugin/src/main/resources/liquibase/keycloak/port/00000000000003_dataPort_production.xml @@ -0,0 +1,28 @@ + + + + + + SELECT COUNT(*) FROM DATABASECHANGELOG WHERE id = '00000000000001_dataPort_restore' + + SELECT COUNT(*) FROM DATABASECHANGELOG WHERE id = '00000000000001_dataPort_production' AND filename = 'liquibase/changeSetPort.xml' + SELECT COUNT(*) FROM categories WHERE catcode = 'home' + + + + + + + + + + + + + diff --git a/keycloak-plugin/src/main/resources/liquibase/keycloak/port/clob/production/sysconfig_kc.xml b/keycloak-plugin/src/main/resources/liquibase/keycloak/port/clob/production/sysconfig_kc.xml new file mode 100644 index 0000000000..f631f4c253 --- /dev/null +++ b/keycloak-plugin/src/main/resources/liquibase/keycloak/port/clob/production/sysconfig_kc.xml @@ -0,0 +1,28 @@ + + + + false + sim730 + CLIENTROLE + false + + + false + AD_ROLE + ROLE + false + + + false + AD_GROUP + GROUP + false + + + false + AD_GROUPROLE + GROUPROLE + _r_ + false + + diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java index 576fc1c429..c4c97580f5 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java @@ -282,6 +282,36 @@ void testDynamicConfigurationGroupRoleOnLoginNoPersist() throws Exception { assertNull(authCaptor.getValue().getRole()); } + @Test + void testDynamicConfigurationGroupRoleOnLoginAlreadyPresent() throws Exception { + Group group = new Group(); + Role role = new Role(); + Authorization auth = new Authorization(group, role); + + group.setName("agroup"); + group.setDescription("agroup"); + role.setName("arole"); + role.setDescription("arole"); + + when(authorizationManager.getUserAuthorizations(anyString())).thenReturn(List.of(auth)); + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(XML_GROUP_ROLE_CONF); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("agroup_r_arole"))); + + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); + + manager.init(); + + final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); + + manager.processNewUser(userDetails, JWT, false); + + verify(authorizationManager, never()).addUserAuthorization(eq("testuser"), authCaptor.capture()); + } + @Test void testDynamicConfigurationNoGroupOnlyRoleOnLogin() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); From 79f10d600a2dad4edffa2d9e231c0bb0af97f193 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Thu, 15 Jan 2026 10:44:48 +0100 Subject: [PATCH 09/15] ESB-890: code cleaning, liquibase support --- keycloak-plugin/pom.xml | 31 +++++++++++++++++++ .../KeycloakAuthorizationManager.java | 2 +- .../entando-keycloak-auth/component.xml | 20 ++++++++++++ .../changeSetPort.xml | 0 .../00000000000003_dataPort_production.xml | 5 ++- .../port/clob/production/sysconfig_kc.xml | 0 6 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 keycloak-plugin/src/main/resources/component/entando-keycloak-auth/component.xml rename keycloak-plugin/src/main/resources/liquibase/{keycloak => entando-keycloak-auth}/changeSetPort.xml (100%) rename keycloak-plugin/src/main/resources/liquibase/{keycloak => entando-keycloak-auth}/port/00000000000003_dataPort_production.xml (86%) rename keycloak-plugin/src/main/resources/liquibase/{keycloak => entando-keycloak-auth}/port/clob/production/sysconfig_kc.xml (100%) diff --git a/keycloak-plugin/pom.xml b/keycloak-plugin/pom.xml index 53a50910c8..69fb56e03c 100644 --- a/keycloak-plugin/pom.xml +++ b/keycloak-plugin/pom.xml @@ -13,6 +13,37 @@ Entando Plugin: Keycloak http://www.entando.com/ + + + src/main/resources + + src/main/resources/component + + + + src/main/resources + + **/*component.xml + + true + + + src/main/java + + **/*.properties + **/*.xml + **/*.xsd + **/*.txt + + + + src/test/java + + **/*.sql + **/*.json + + + org.apache.maven.plugins diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index 88c8174d72..e0f903fab8 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -100,7 +100,7 @@ public void init() throws Exception { } } if (activeMappings != null) { - log.info("Dynamic configuration processed: {} active elements found", activeMappings.size()); + activeMappings.forEach(m -> log.debug("mapping active: {}", m.toString())); } } catch (Exception e) { log.error("Error initializing KeycloakAuthorizationManager", e); diff --git a/keycloak-plugin/src/main/resources/component/entando-keycloak-auth/component.xml b/keycloak-plugin/src/main/resources/component/entando-keycloak-auth/component.xml new file mode 100644 index 0000000000..2134d388b9 --- /dev/null +++ b/keycloak-plugin/src/main/resources/component/entando-keycloak-auth/component.xml @@ -0,0 +1,20 @@ + + + entando-keycloak-auth + Entando CMS + + ${project.artifactId} + ${project.groupId} + ${project.version} + + + + + sysconfig
+
+
+ + + +
+
diff --git a/keycloak-plugin/src/main/resources/liquibase/keycloak/changeSetPort.xml b/keycloak-plugin/src/main/resources/liquibase/entando-keycloak-auth/changeSetPort.xml similarity index 100% rename from keycloak-plugin/src/main/resources/liquibase/keycloak/changeSetPort.xml rename to keycloak-plugin/src/main/resources/liquibase/entando-keycloak-auth/changeSetPort.xml diff --git a/keycloak-plugin/src/main/resources/liquibase/keycloak/port/00000000000003_dataPort_production.xml b/keycloak-plugin/src/main/resources/liquibase/entando-keycloak-auth/port/00000000000003_dataPort_production.xml similarity index 86% rename from keycloak-plugin/src/main/resources/liquibase/keycloak/port/00000000000003_dataPort_production.xml rename to keycloak-plugin/src/main/resources/liquibase/entando-keycloak-auth/port/00000000000003_dataPort_production.xml index a0c7732156..8d2677a621 100644 --- a/keycloak-plugin/src/main/resources/liquibase/keycloak/port/00000000000003_dataPort_production.xml +++ b/keycloak-plugin/src/main/resources/liquibase/entando-keycloak-auth/port/00000000000003_dataPort_production.xml @@ -8,11 +8,10 @@ - SELECT COUNT(*) FROM DATABASECHANGELOG WHERE id = '00000000000001_dataPort_restore' + SELECT COUNT(*) FROM DATABASECHANGELOG WHERE id = '00000000000003_dataPort_production' - SELECT COUNT(*) FROM DATABASECHANGELOG WHERE id = '00000000000001_dataPort_production' AND filename = 'liquibase/changeSetPort.xml' - SELECT COUNT(*) FROM categories WHERE catcode = 'home' + SELECT COUNT(*) FROM DATABASECHANGELOG WHERE id = '00000000000003_dataPort_production' AND filename = 'liquibase/changeSetPort.xml' diff --git a/keycloak-plugin/src/main/resources/liquibase/keycloak/port/clob/production/sysconfig_kc.xml b/keycloak-plugin/src/main/resources/liquibase/entando-keycloak-auth/port/clob/production/sysconfig_kc.xml similarity index 100% rename from keycloak-plugin/src/main/resources/liquibase/keycloak/port/clob/production/sysconfig_kc.xml rename to keycloak-plugin/src/main/resources/liquibase/entando-keycloak-auth/port/clob/production/sysconfig_kc.xml From 1838213fad4f9ebe06ca5a40edd4721c58096a40 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Thu, 15 Jan 2026 11:11:43 +0100 Subject: [PATCH 10/15] ESB-890: code cleaning --- .../KeycloakAuthorizationManager.java | 76 +++++++++++-------- .../keycloak/filter/KeycloakFilterTest.java | 2 +- .../KeycloakAuthorizationManagerTest.java | 6 +- 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index e0f903fab8..f23a95936c 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -23,7 +23,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; +import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; @@ -36,6 +38,7 @@ import org.entando.entando.ent.util.EntLogging.EntLogger; import org.entando.entando.keycloak.services.mapping.DynamicMapping; import org.entando.entando.keycloak.services.mapping.DynamicMappingElement; +import org.entando.entando.keycloak.services.mapping.DynamicMappingKind; import org.entando.entando.keycloak.services.oidc.model.KeycloakUser; import org.springframework.beans.factory.annotation.Autowired; @@ -146,41 +149,19 @@ private boolean isValid(DynamicMappingElement elem) { return true; } - public void processNewUser(final UserDetails user, final String token, boolean decode) { + public void processNewUser(final UserDetails user, final String token, final boolean decode) { processNewUser(user); readLock.lock(); try { - final DynamicMappingElement tokenMapper = ofNullable(activeMappings) + final List tokenMapper = ofNullable(activeMappings) .orElse(emptyList()) .stream() .filter(m -> m.kind == CLIENTROLE) - .findFirst().orElse(null); - // process client claim... - if (StringUtils.isNotBlank(token) && tokenMapper != null) { - final String payload = decode ? token.split("\\.")[1] : token; - final String json = decode ? new String(Base64.getUrlDecoder().decode(payload)) : payload; - - try { - final JsonNode root = mapper.readTree(json); - - JsonNode roleNode = root - .path("resource_access") - .path(tokenMapper.client) - .path("roles"); - - if (roleNode == null) { - return; - } - - List roles = StreamSupport.stream(roleNode.spliterator(), false) - .map(JsonNode::asText) - .collect(Collectors.toList()); - if (user instanceof KeycloakUser) { - finalizeRoleAssociation((KeycloakUser) user, tokenMapper, roles); - } - - } catch (Exception e) { - log.error("error importing client role into Entando roles", e); + .collect(Collectors.toList()); + // process client claims... + if (StringUtils.isNotBlank(token) && !tokenMapper.isEmpty()) { + for (DynamicMappingElement cur: tokenMapper) { + processClaimAttributes(user, token, decode, cur); } } // ...then process attributes coming from the user profile @@ -194,7 +175,42 @@ public void processNewUser(final UserDetails user, final String token, boolean d } } - public void processNewUser(final UserDetails user) { + /** + * Analyze the JWT looking for known mappings to translate into Entando roles + * @param user logged in user + * @param token access token + * @param decode is true the access token is decoded from the base64 form + * @param tokenMapper the mapping configuration + */ + private void processClaimAttributes(UserDetails user, String token, boolean decode, DynamicMappingElement tokenMapper) { + final String payload = decode ? token.split("\\.")[1] : token; + final String json = decode ? new String(Base64.getUrlDecoder().decode(payload)) : payload; + + try { + final JsonNode root = mapper.readTree(json); + + JsonNode roleNode = root + .path("resource_access") + .path(tokenMapper.client) + .path("roles"); + + if (roleNode == null) { + return ; + } + + List roles = StreamSupport.stream(roleNode.spliterator(), false) + .map(JsonNode::asText) + .collect(Collectors.toList()); + if (user instanceof KeycloakUser) { + finalizeRoleAssociation((KeycloakUser) user, tokenMapper, roles); + } + + } catch (Exception e) { + log.error("error importing client role into Entando roles", e); + } + } + + private void processNewUser(final UserDetails user) { if (StringUtils.isNotEmpty(configuration.getDefaultAuthorizations())) { // process group and role coming from the configuration final Set defaultAuthorizations = Sets.newHashSet(configuration.getDefaultAuthorizations().split(",")); diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/filter/KeycloakFilterTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/filter/KeycloakFilterTest.java index 7c162bde02..6fec89432d 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/filter/KeycloakFilterTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/filter/KeycloakFilterTest.java @@ -160,7 +160,7 @@ void testAuthenticationFlow() throws IOException, ServletException, EntException verify(oidcService, times(1)).requestToken(eq(authorizationCode), eq(loginEndpoint)); verify(oidcService, times(1)).validateToken(eq("access-token-over-here")); - verify(keycloakGroupManager, times(1)).processNewUser(same(userDetails)); + verify(keycloakGroupManager, times(1)).processNewUser(same(userDetails), null, false); verify(session, times(1)).setAttribute(eq("user"), same(userDetails)); verify(session, times(1)).setAttribute(eq(SystemConstants.SESSIONPARAM_CURRENT_USER), same(userDetails)); diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java index c4c97580f5..1a0901b021 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java @@ -57,7 +57,7 @@ void testGroupCreation() throws EntException { when(groupManager.getGroup(anyString())).thenReturn(null); when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); - manager.processNewUser(userDetails); + manager.processNewUser(userDetails, null, false); final ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(Group.class); final ArgumentCaptor auth_man_usernameCaptor = ArgumentCaptor.forClass(String.class); @@ -86,7 +86,7 @@ void testGroupAndRoleCreation() throws EntException { when(roleManager.getRole(anyString())).thenReturn(null); when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); - manager.processNewUser(userDetails); + manager.processNewUser(userDetails, null, false); final ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(Group.class); final ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(Role.class); @@ -119,7 +119,7 @@ void testVerification() { when(configuration.getDefaultAuthorizations()).thenReturn("readers:read-all,writers:write-all"); when(userDetails.getAuthorizations()).thenReturn(Arrays.asList(readers, writers)); - manager.processNewUser(userDetails); + manager.processNewUser(userDetails, null, false); verify(roleManager, times(0)).getRole(anyString()); verify(groupManager, times(0)).getGroup(anyString()); From e7ff1195c0380b68a83794b78374c2226a0ee1e4 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Thu, 15 Jan 2026 12:06:44 +0100 Subject: [PATCH 11/15] ESB-890: quality gate --- .../keycloak/services/KeycloakAuthorizationManager.java | 7 ++----- .../entando/keycloak/services/KeycloakService.java | 3 +-- .../keycloak/services/mapping/DynamicMappingElement.java | 4 ++-- .../keycloak/services/mapping/DynamicMappingKind.java | 2 +- .../services/KeycloakAuthorizationManagerTest.java | 8 +------- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index f23a95936c..447f37c84d 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -25,7 +25,6 @@ import java.util.Base64; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; @@ -38,11 +37,9 @@ import org.entando.entando.ent.util.EntLogging.EntLogger; import org.entando.entando.keycloak.services.mapping.DynamicMapping; import org.entando.entando.keycloak.services.mapping.DynamicMappingElement; -import org.entando.entando.keycloak.services.mapping.DynamicMappingKind; import org.entando.entando.keycloak.services.oidc.model.KeycloakUser; import org.springframework.beans.factory.annotation.Autowired; -//@Service public class KeycloakAuthorizationManager extends AbstractService { private static final EntLogger log = EntLogFactory.getSanitizedLogger(KeycloakAuthorizationManager.class); @@ -96,7 +93,7 @@ public void init() throws Exception { activeMappings = dynConf.mapping .stream() .filter(this::isValid) - .filter(d -> (d.enabled != null && d.enabled)) + .filter(d -> (d.enabled)) .collect(Collectors.toUnmodifiableList()); log.debug("{} dynamic auth mapping found, {} activeMappings", dynConf.mapping.size(), activeMappings.size()); @@ -462,7 +459,7 @@ private static List processUserProfileAttribute(KeycloakUser user, Dynam || user.getUserRepresentation().getAttributes() == null || !user.getUserRepresentation().getAttributes().containsKey(elem.attribute)) { log.info("skipping dynamic processing for user {}", user.getUsername()); - return null; + return Collections.emptyList(); } final Object kcProfileAttr = user.getUserRepresentation() .getAttributes() diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java index 3453eeaf80..43d84e0652 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakService.java @@ -206,10 +206,9 @@ private ResponseEntity executeEscapedRequest(String token, final Strin queryBuilder.append(key).append("=").append(value); }); String escapedUrl = builder.build().toUri() + "?" + queryBuilder; - ResponseEntity retval = restTemplate.exchange( + return restTemplate.exchange( URI.create(escapedUrl), method, createEntity(token, entity.getBody()), result); - return retval; } } catch (HttpClientErrorException e) { if (HttpStatus.FORBIDDEN.equals(e.getStatusCode()) || (HttpStatus.UNAUTHORIZED.equals(e.getStatusCode()) && retryCount > 10)) { diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java index 2a56525753..bddf07f618 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java @@ -3,11 +3,11 @@ public class DynamicMappingElement { - public Boolean enabled; + public boolean enabled; public String attribute; public DynamicMappingKind kind; public String injectTo; - public Boolean persist; + public boolean persist; public String separator; // FOR GROUPROLE ONLY public String client; // FOR CLIENTROLE ONLY diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java index 11ed339be2..cc34366429 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java @@ -11,7 +11,7 @@ public enum DynamicMappingKind { GROUPROLE("grouprole"), CLIENTROLE("clientrole"); - public String kind; + private String kind; DynamicMappingKind(String kind) { this.kind = kind; } diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java index 1a0901b021..050b9ae5b2 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java @@ -47,7 +47,7 @@ class KeycloakAuthorizationManagerTest { private KeycloakAuthorizationManager manager; @BeforeEach - public void setUp() throws Exception { + public void setUp() { manager = new KeycloakAuthorizationManager(configuration, authorizationManager, groupManager, roleManager, configManager); } @@ -159,8 +159,6 @@ void testDynamicConfigurationRoleOnLoginFromJwt() throws Exception { when(userDetails.getUsername()).thenReturn("testuser"); when(configManager.getConfigItem(anyString())).thenReturn(XML_CLIENT_ROLE); - UserRepresentation userRepresentation = new UserRepresentation(); - manager.init(); final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); @@ -178,8 +176,6 @@ void testDynamicConfigurationRoleOnLoginWithWrongJwt() throws Exception { when(configuration.getDefaultAuthorizations()).thenReturn(null); when(configManager.getConfigItem(anyString())).thenReturn(XML_CLIENT_ROLE); - UserRepresentation userRepresentation = new UserRepresentation(); - manager.init(); final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); @@ -324,8 +320,6 @@ void testDynamicConfigurationNoGroupOnlyRoleOnLogin() throws Exception { manager.init(); - final ArgumentCaptor authCaptor = ArgumentCaptor.forClass(Authorization.class); - manager.processNewUser(userDetails, JWT, false); verify(authorizationManager, never()).addUserAuthorization(anyString(), any()); From ac215e276f55d391329830ccb108b99b31e51f50 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Thu, 15 Jan 2026 12:09:47 +0100 Subject: [PATCH 12/15] ESB-890: quality gate --- .../entando/keycloak/services/mapping/DynamicMappingKind.java | 2 +- .../entando/entando/keycloak/filter/KeycloakFilterTest.java | 3 +-- .../keycloak/services/KeycloakAuthorizationManagerTest.java | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java index cc34366429..e9f0430afe 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java @@ -11,7 +11,7 @@ public enum DynamicMappingKind { GROUPROLE("grouprole"), CLIENTROLE("clientrole"); - private String kind; + private final String kind; DynamicMappingKind(String kind) { this.kind = kind; } diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/filter/KeycloakFilterTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/filter/KeycloakFilterTest.java index 6fec89432d..99b0d5c03c 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/filter/KeycloakFilterTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/filter/KeycloakFilterTest.java @@ -28,7 +28,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import org.apache.commons.lang3.StringUtils; import org.assertj.core.api.AbstractBooleanAssert; import org.assertj.core.api.AbstractCharSequenceAssert; import org.entando.entando.aps.system.services.tenants.ITenantManager; @@ -169,7 +168,7 @@ void testAuthenticationFlow() throws IOException, ServletException, EntException } @Test - void testAuthenticationFlowWithError() throws IOException, ServletException { + void testAuthenticationFlowWithError() { final String loginEndpoint = "https://dev.entando.org/entando-app/do/login"; final String state = "0ca97afd-f0b0-4860-820a-b7cd1414f69c"; final String authorizationCode = "the-authorization-code-from-keycloak"; diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java index 050b9ae5b2..c9ccce4b16 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java @@ -42,7 +42,6 @@ class KeycloakAuthorizationManagerTest { @Mock private GroupManager groupManager; @Mock private RoleManager roleManager; @Mock private BaseConfigManager configManager; - @Mock private KeycloakUserManager userManager; private KeycloakAuthorizationManager manager; From 15fc343b2d9eb151740d61d8232ff3c7d9b786ab Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Thu, 15 Jan 2026 17:45:52 +0100 Subject: [PATCH 13/15] ESB-890: improved configuration handling --- .../KeycloakAuthorizationManager.java | 48 +++++++++++-------- .../services/mapping/DynamicMappingKind.java | 16 ++++--- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java index 447f37c84d..57d277d6ef 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java @@ -25,6 +25,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; @@ -79,28 +80,34 @@ public KeycloakAuthorizationManager(final KeycloakConfiguration configuration, * either by reloading the global configuration or after a certain amount of time (by default, * one minute) */ - private transient List activeMappings; + private transient List profileMappings; + private transient List jwtMappings; @Override public void init() throws Exception { writeLock.lock(); + profileMappings = new ArrayList<>(); + jwtMappings = new ArrayList<>(); try { String xml = configManager.getConfigItem("dynamicAuthMapping"); if (StringUtils.isNotBlank(xml)) { DynamicMapping dynConf = xmlMapper.readValue(xml, DynamicMapping.class); if (dynConf != null && dynConf.mapping != null) { - activeMappings = dynConf.mapping - .stream() - .filter(this::isValid) - .filter(d -> (d.enabled)) - .collect(Collectors.toUnmodifiableList()); - log.debug("{} dynamic auth mapping found, {} activeMappings", - dynConf.mapping.size(), activeMappings.size()); + Map> partitioned = + dynConf.mapping.stream() + .filter(this::isValid) + .collect(Collectors.partitioningBy( + item -> item.kind.isJwtMapping() + )); + profileMappings = List.copyOf(partitioned.get(false)); + jwtMappings = List.copyOf(partitioned.get(true)); + log.debug("{} dynamic auth mapping found, {} profileMappings", + dynConf.mapping.size(), profileMappings.size()); } } - if (activeMappings != null) { - activeMappings.forEach(m -> log.debug("mapping active: {}", m.toString())); + if (profileMappings != null) { + profileMappings.forEach(m -> log.debug("mapping active: {}", m.toString())); } } catch (Exception e) { log.error("Error initializing KeycloakAuthorizationManager", e); @@ -150,21 +157,22 @@ public void processNewUser(final UserDetails user, final String token, final boo processNewUser(user); readLock.lock(); try { - final List tokenMapper = ofNullable(activeMappings) + // TODO for the future: handle also groups ~ "claim to group import" type + final List jwtRoleMapper = ofNullable(jwtMappings) .orElse(emptyList()) .stream() .filter(m -> m.kind == CLIENTROLE) .collect(Collectors.toList()); - // process client claims... - if (StringUtils.isNotBlank(token) && !tokenMapper.isEmpty()) { - for (DynamicMappingElement cur: tokenMapper) { - processClaimAttributes(user, token, decode, cur); + // process client role claims, if any... + if (StringUtils.isNotBlank(token) && !jwtRoleMapper.isEmpty()) { + for (DynamicMappingElement cur: jwtRoleMapper) { + processRoleClaimAttributes(user, token, decode, cur); } } - // ...then process attributes coming from the user profile + // ...then process attributes coming from the user profile, if needed if (user instanceof KeycloakUser - && activeMappings != null - && !activeMappings.isEmpty()) { + && profileMappings != null + && !profileMappings.isEmpty()) { processProfileAttributes((KeycloakUser) user); } } finally { @@ -179,7 +187,7 @@ public void processNewUser(final UserDetails user, final String token, final boo * @param decode is true the access token is decoded from the base64 form * @param tokenMapper the mapping configuration */ - private void processClaimAttributes(UserDetails user, String token, boolean decode, DynamicMappingElement tokenMapper) { + private void processRoleClaimAttributes(UserDetails user, String token, boolean decode, DynamicMappingElement tokenMapper) { final String payload = decode ? token.split("\\.")[1] : token; final String json = decode ? new String(Base64.getUrlDecoder().decode(payload)) : payload; @@ -276,7 +284,7 @@ private synchronized Role findOrCreateRole(final String roleName) { * @param user the currently logged user */ private synchronized void processProfileAttributes(final KeycloakUser user) { - activeMappings.forEach(m -> { + profileMappings.forEach(m -> { if (m.kind == ROLE) { doProcessRole(user, m); } diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java index e9f0430afe..3cce689f39 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingKind.java @@ -3,17 +3,22 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import java.util.Arrays; +import lombok.Getter; public enum DynamicMappingKind { - GROUP("group"), - ROLE("role"), - GROUPROLE("grouprole"), - CLIENTROLE("clientrole"); + GROUP("group", false), + ROLE("role", false), + GROUPROLE("grouprole", false), + CLIENTROLE("clientrole", true); private final String kind; - DynamicMappingKind(String kind) { + @Getter + private final boolean jwtMapping; + + DynamicMappingKind(String kind, boolean jwtmapping) { this.kind = kind; + this.jwtMapping = jwtmapping; } @JsonValue @@ -29,5 +34,4 @@ public static DynamicMappingKind fromValue(String value) { .orElseThrow(() -> new IllegalArgumentException("Unknown DynamicMappingKind: " + value)); } - } From f518233432f0f53ed358e44d10c8c7c4cff058d5 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Fri, 16 Jan 2026 09:23:09 +0100 Subject: [PATCH 14/15] ESB-890: README.md --- keycloak-plugin/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keycloak-plugin/README.md b/keycloak-plugin/README.md index b43b7c72a4..be76c52d5e 100644 --- a/keycloak-plugin/README.md +++ b/keycloak-plugin/README.md @@ -10,6 +10,7 @@ Information below is for building from source or running locally as a contributo ### What this plugin does * Enables SSO capabilities to an Entando Instance by using Keycloak. * Moves User Management to Keycloak. +* Assigns on-the-fly authorizations to the logging-in users; such authorizations might from keycloak itself or external sources ### What this plugin does not This plugin doesn't come with Role and Group management, because Entando Core roles/groups model isn't compatible with Keycloak. That means that even with the same users across multiple Entando Instances, the role and group mappings have to be configured on each instance. @@ -24,6 +25,10 @@ This plugin doesn't come with Role and Group management, because Entando Core ro >- `keycloak.secure.uris`: **[OPTIONAL]** Use if you want to secure an endpoint. Works with wildcards, comma separated. >- `keycloak.authenticated.user.default.authorizations`: **[OPTIONAL]** Use if you want to automatically assign `group:role` to any user that logs in, comma separated. Example: `administrators:admin,readers` +## Environment variables +>- `KC_MAPPING_UPDATE`: specifies the refresh period -Chron style!- of the dynamic configuration used to assign authorizations to the loggin-in users. The default is `0 * * * * *` + + ## Installing ### Installing on your project From 13b190fca3f0142cee1c60156d59cfa614976047 Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Fri, 16 Jan 2026 09:26:02 +0100 Subject: [PATCH 15/15] ESB-890: README.md --- keycloak-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak-plugin/README.md b/keycloak-plugin/README.md index be76c52d5e..52676521c9 100644 --- a/keycloak-plugin/README.md +++ b/keycloak-plugin/README.md @@ -10,7 +10,7 @@ Information below is for building from source or running locally as a contributo ### What this plugin does * Enables SSO capabilities to an Entando Instance by using Keycloak. * Moves User Management to Keycloak. -* Assigns on-the-fly authorizations to the logging-in users; such authorizations might from keycloak itself or external sources +* Assigns on-the-fly authorizations to the logging-in users; such authorizations might come from keycloak itself or external sources ### What this plugin does not This plugin doesn't come with Role and Group management, because Entando Core roles/groups model isn't compatible with Keycloak. That means that even with the same users across multiple Entando Instances, the role and group mappings have to be configured on each instance.