From 1f7ebbcec69574b55591d8907148dfb04754d6ca Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Fri, 26 Jun 2026 10:58:10 +0200 Subject: [PATCH] ESB-1126 Improved keycloak configuration handling --- .../KeycloakAuthorizationManager.java | 107 +++-- .../services/KeycloakImportConfig.java | 1 + .../services/mapping/DynamicMapping.java | 3 + .../mapping/DynamicMappingElement.java | 1 + .../services/mapping/FallbackKind.java | 30 ++ .../KeycloakAuthorizationManagerTest.java | 367 ++++++++++++++++++ 6 files changed, 475 insertions(+), 34 deletions(-) create mode 100644 keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/FallbackKind.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 86fd6f20e..1208f1598 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 @@ -7,6 +7,8 @@ import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.ROLEGROUP; import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.ROLEGROUPCLAIM; +import java.util.regex.Pattern; + import com.agiletec.aps.system.common.AbstractService; import com.agiletec.aps.system.services.authorization.Authorization; import com.agiletec.aps.system.services.authorization.AuthorizationManager; @@ -37,6 +39,7 @@ 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.mapping.FallbackKind; import org.entando.entando.keycloak.services.mapping.PersistKind; import org.entando.entando.keycloak.services.oidc.OidcMappingHelper; import org.entando.entando.keycloak.services.oidc.model.KeycloakUser; @@ -102,6 +105,7 @@ public void initTenantAware() throws Exception { List ignore = new ArrayList<>(); List roles = new ArrayList<>(); List groups = new ArrayList<>(); + List excludeUsers = new ArrayList<>(); Boolean enabled = false; PersistKind persist = PersistKind.FULL; @@ -130,6 +134,8 @@ public void initTenantAware() throws Exception { .orElseGet(List::of); groups = ofNullable(dynConf.groups) .orElse(List.of()); + excludeUsers = ofNullable(dynConf.excludeUsers) + .orElse(List.of()); enabled = ofNullable(dynConf.enabled) .orElse(false); persist = ofNullable(dynConf.persist) @@ -143,12 +149,12 @@ public void initTenantAware() throws Exception { jwtMappings.forEach(m -> log.debug("jwt mapping active: {}", m.toString())); } // finally - KeycloakImportConfig cfg = new KeycloakImportConfig(profileMappings, jwtMappings, ignore, roles, groups, enabled, persist); + KeycloakImportConfig cfg = new KeycloakImportConfig(profileMappings, jwtMappings, ignore, roles, groups, enabled, persist, excludeUsers); setImportConfiguration(cfg); } catch (Exception e) { log.error("Error initializing KeycloakAuthorizationManager", e); - KeycloakImportConfig cfg = new KeycloakImportConfig(List.of(), List.of(), List.of(), List.of(), List.of(), false, PersistKind.NONE); + KeycloakImportConfig cfg = new KeycloakImportConfig(List.of(), List.of(), List.of(), List.of(), List.of(), false, PersistKind.NONE, List.of()); setImportConfiguration(cfg); throw e; @@ -190,23 +196,36 @@ public void cleanSyncData() { * @return true if the element is valid, false otherwise */ private boolean isValid(DynamicMappingElement elem) { - if (elem.kind == null) { - log.error("invalid dynamic mapping element, 'kind' is blank"); - return false; - } - if (StringUtils.isBlank(elem.attribute) && !elem.kind.isJwtMapping()) { - log.error("invalid dynamic mapping element, 'attribute' is blank for kind {}", elem.kind); - return false; - } - if (StringUtils.isBlank(elem.path) && elem.kind.isJwtMapping()) { - log.error("invalid dynamic mapping element, 'path' is blank for {} kind", elem.kind); - return false; - } - if (StringUtils.isBlank(elem.separator) && (elem.kind == ROLEGROUP || elem.kind == ROLEGROUPCLAIM)) { - log.error("invalid dynamic mapping element, 'separator' is blank for {} kind", elem.kind); + try { + if (elem == null) { + log.error("invalid dynamic mapping element, element is null"); + return false; + } + if (elem.kind == null) { + log.error("invalid dynamic mapping element, 'kind' is missing"); + return false; + } + if (StringUtils.isBlank(elem.attribute) && !elem.kind.isJwtMapping()) { + log.error("invalid dynamic mapping element, 'attribute' is blank for kind {}", elem.kind); + return false; + } + if (StringUtils.isBlank(elem.path) && elem.kind.isJwtMapping()) { + log.error("invalid dynamic mapping element, 'path' is blank for {} kind", elem.kind); + return false; + } + if (StringUtils.isBlank(elem.separator) && (elem.kind == ROLEGROUP || elem.kind == ROLEGROUPCLAIM)) { + log.error("invalid dynamic mapping element, 'separator' is blank for {} kind", elem.kind); + return false; + } + if (elem.fallback != null && elem.kind != ROLEGROUP && elem.kind != ROLEGROUPCLAIM) { + log.warn("dynamic mapping element has 'fallback' set for kind {} which does not support it; " + + "'fallback' is only applicable to {} and {}", elem.kind, ROLEGROUP, ROLEGROUPCLAIM); + } + return true; + } catch (Exception e) { + log.error("Unexpected error validating dynamic mapping element", e); return false; } - return true; } public void processNewUser(final UserDetails user, final String token, final boolean decode) { @@ -216,6 +235,11 @@ public void processNewUser(final UserDetails user, final String token, final boo readLock.lock(); try { + final List excludedUsers = getImportConfiguration().getExcludeUsers(); + if (excludedUsers != null && user.getUsername() != null && excludedUsers.contains(user.getUsername())) { + log.debug("user {} is in the excludeUsers list, skipping dynamic sync", user.getUsername()); + return; + } if (!getImportConfiguration().getEnabled()) return; // Authorizations coming from dynamic mapping (that is, external sources) @@ -336,19 +360,18 @@ private List finalizeGroupRoleAssociation(KeycloakUser user, Dyna } final String sep = StringUtils.isNotBlank(elem.separator) ? elem.separator : DEFAULT_SEPARATOR; - final String[] tokens = candidate.split(sep); - if (tokens.length < 2) { - // treat as a role -// result.addAll(finalizeRoleAssociation(user, elem, List.of(candidate.trim()))); + if (!candidate.contains(sep)) { + result.addAll(applyFallbackForRoleGroup(user, elem, candidate.trim())); continue; } + final String[] tokens = candidate.split(Pattern.quote(sep), -1); final String roleName = tokens[0].trim(); - final String groupName = tokens[1].trim(); + final String groupName = tokens.length > 1 ? tokens[1].trim() : ""; - if (StringUtils.isBlank(roleName)) { - log.warn("Invalid role name extracted from candidate '{}' for user {}", candidate, user.getUsername()); + if (StringUtils.isBlank(roleName) || StringUtils.isBlank(groupName)) { + log.warn("Blank role or group in token '{}' for user {}, discarding", candidate, user.getUsername()); continue; } @@ -363,6 +386,20 @@ private List finalizeGroupRoleAssociation(KeycloakUser user, Dyna return result; } + private List applyFallbackForRoleGroup(KeycloakUser user, DynamicMappingElement elem, String token) { + if (elem.fallback == FallbackKind.IGNORE) { + log.warn("No separator in token '{}' for user {}, discarding (fallback=ignore)", token, user.getUsername()); + return List.of(); + } + if (elem.fallback == FallbackKind.GROUP) { + return finalizeGroupAssociation(user, elem, List.of(token)); + } + if (elem.fallback == null) { + log.info("No separator in token '{}' for user {}, treating as role (implicit default fallback)", token, user.getUsername()); + } + return finalizeRoleAssociation(user, elem, List.of(token)); + } + private void processNewUser(final UserDetails user) { if (StringUtils.isNotEmpty(KeycloackConfiguration.getDefaultAuthorizations())) { // process group and role coming from the configuration @@ -471,6 +508,10 @@ private List doProcessRoleGroup(KeycloakUser user, DynamicMapping return result; } for (String groupRoleToken : authorizations) { + if (!groupRoleToken.contains(separator)) { + result.addAll(applyFallbackForRoleGroup(user, elem, groupRoleToken.trim())); + continue; + } Authorization auth = parseAuthForRoleGroup(user, groupRoleToken, separator); if (auth != null) { result.add(auth); @@ -483,18 +524,16 @@ private List doProcessRoleGroup(KeycloakUser user, DynamicMapping } private Authorization parseAuthForRoleGroup(KeycloakUser user, String groupRoleToken, String separator) { - final String[] tokens = groupRoleToken.split(separator); + final String[] tokens = groupRoleToken.split(Pattern.quote(separator), -1); + + final String roleName = tokens[0].trim(); + final String groupName = tokens.length > 1 ? tokens[1].trim() : ""; - if (tokens.length != 2 - || StringUtils.isBlank(tokens[0]) - || StringUtils.isBlank(tokens[1])) { - log.error("invalid dynamic config configuration detected"); + if (StringUtils.isBlank(roleName) || StringUtils.isBlank(groupName)) { + log.warn("Blank role or group in token '{}' for user {}, discarding", groupRoleToken, user.getUsername()); return null; } - final String groupName = tokens[1]; - final String roleName = tokens[0]; - return finalizeAssociation(user, roleName, groupName, false); } @@ -553,11 +592,11 @@ private Authorization finalizeAssociation(KeycloakUser user, String roleName, St } // are they managed? if (StringUtils.isNotBlank(roleName) && !getImportConfiguration().getRoles().contains(roleName)) { - log.info("Role {} is not managed. Skipping assignment for user {}", roleName, user.getUsername()); + log.warn("Role {} is not in the roles allowlist. Skipping assignment for user {}", roleName, user.getUsername()); return null; } if (StringUtils.isNotBlank(groupName) && !getImportConfiguration().getGroups().contains(groupName)) { - log.info("Group {} is not managed. Skipping assignment for user {}", groupName, user.getUsername()); + log.warn("Group {} is not in the groups allowlist. Skipping assignment for user {}", groupName, user.getUsername()); return null; } return createAuthorization(roleName, groupName, createRoleIfMissing); diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java index 81053f64c..d1d64d839 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java @@ -20,5 +20,6 @@ public class KeycloakImportConfig { private transient List groups; private transient Boolean enabled; private transient PersistKind persist; + private transient List excludeUsers; } 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 ba5b71dbc..afdaff265 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 @@ -21,6 +21,9 @@ public class DynamicMapping { @JacksonXmlElementWrapper(localName = "groups") public List groups; + @JacksonXmlElementWrapper(localName = "excludeUsers") + public List excludeUsers; + public Boolean enabled; public PersistKind persist; 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 229f180a2..6493c3a9d 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 @@ -7,6 +7,7 @@ public class DynamicMappingElement { public String attribute; public DynamicMappingKind kind; public String separator; // FOR ROLEGROUP and ROLEGROUPCLAIM ONLY + public FallbackKind fallback; // FOR ROLEGROUP and ROLEGROUPCLAIM ONLY public String path; // FOR *CLAIM ONLY } diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/FallbackKind.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/FallbackKind.java new file mode 100644 index 000000000..129684598 --- /dev/null +++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/FallbackKind.java @@ -0,0 +1,30 @@ +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 FallbackKind { + ROLE("role"), + GROUP("group"), + IGNORE("ignore"); + + private final String kind; + + FallbackKind(String kind) { + this.kind = kind; + } + + @JsonValue + public String getXmlValue() { + return kind; + } + + @JsonCreator + public static FallbackKind fromValue(String value) { + return Arrays.stream(values()) + .filter(k -> k.kind.equalsIgnoreCase(value)) + .findFirst() + .orElse(null); + } +} 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 fd9dcd2cf..0ee40a118 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 @@ -1078,6 +1078,156 @@ void testExternalAuthSyncArgsVerifiedExactly() throws Exception { assertThat(toDeleteCaptor.getValue()).isEmpty(); } + // ------------------------------------------------------------------------- + // isValid tests — enabled mappings with missing required fields + // ------------------------------------------------------------------------- + + @Test + void testIsValidFiltersEnabledMappingWithNullKind() throws Exception { + String xml = "" + + " true" + + "" + + " true" + + "" + + ""; + when(configManager.getConfigItem(anyString())).thenReturn(xml); + + manager.init(); + + assertNotNull(manager.getImportConfiguration()); + assertThat(manager.getImportConfiguration().getEnabled()).isTrue(); + assertThat(manager.getImportConfiguration().getProfileMappings()).isEmpty(); + assertThat(manager.getImportConfiguration().getJwtMappings()).isEmpty(); + } + + @Test + void testIsValidFiltersEnabledMappingWithMissingAttribute() throws Exception { + String xml = "" + + " true" + + "" + + " truerole" + + "" + + ""; + when(configManager.getConfigItem(anyString())).thenReturn(xml); + + manager.init(); + + assertNotNull(manager.getImportConfiguration()); + assertThat(manager.getImportConfiguration().getEnabled()).isTrue(); + assertThat(manager.getImportConfiguration().getProfileMappings()).isEmpty(); + } + + @Test + void testIsValidFiltersEnabledMappingWithMissingPath() throws Exception { + String xml = "" + + " true" + + "" + + " trueroleclaim" + + "" + + ""; + when(configManager.getConfigItem(anyString())).thenReturn(xml); + + manager.init(); + + assertNotNull(manager.getImportConfiguration()); + assertThat(manager.getImportConfiguration().getEnabled()).isTrue(); + assertThat(manager.getImportConfiguration().getJwtMappings()).isEmpty(); + } + + @Test + void testIsValidFiltersEnabledMappingWithMissingSeparator() throws Exception { + String xml = "" + + " true" + + "" + + " trueAD_GROUPROLErolegroup" + + "" + + ""; + when(configManager.getConfigItem(anyString())).thenReturn(xml); + + manager.init(); + + assertNotNull(manager.getImportConfiguration()); + assertThat(manager.getImportConfiguration().getEnabled()).isTrue(); + assertThat(manager.getImportConfiguration().getProfileMappings()).isEmpty(); + } + + // ------------------------------------------------------------------------- + // isValid — fallback on unsupported kind: warning only, mapping kept + // ------------------------------------------------------------------------- + + @Test + void testIsValidWarnsFallbackOnUnsupportedKind() throws Exception { + String xml = "" + + " true" + + "" + + " " + + " true" + + " realm_access.roles" + + " roleclaim" + + " role" + + " " + + "" + + "generico" + + ""; + when(configManager.getConfigItem(anyString())).thenReturn(xml); + + manager.init(); + + assertNotNull(manager.getImportConfiguration()); + // The mapping must NOT be rejected — fallback warning is non-fatal + assertThat(manager.getImportConfiguration().getJwtMappings()).hasSize(1); + } + + // ------------------------------------------------------------------------- + // Case-insensitivity: uppercase kind and persist identifiers + // ------------------------------------------------------------------------- + + @Test + void testConfigAcceptsUppercaseKind() throws Exception { + String xml = "" + + " true" + + " full" + + "" + + " " + + " true" + + " realm_access.roles" + + " ROLECLAIM" + + " " + + "" + + "generico" + + ""; + when(configManager.getConfigItem(anyString())).thenReturn(xml); + + manager.init(); + + assertNotNull(manager.getImportConfiguration()); + assertThat(manager.getImportConfiguration().getEnabled()).isTrue(); + assertThat(manager.getImportConfiguration().getJwtMappings()).hasSize(1); + } + + @Test + void testConfigAcceptsUppercasePersist() throws Exception { + String xml = "" + + " true" + + " FULL" + + "" + + " " + + " true" + + " realm_access.roles" + + " roleclaim" + + " " + + "" + + "generico" + + ""; + when(configManager.getConfigItem(anyString())).thenReturn(xml); + + manager.init(); + + assertNotNull(manager.getImportConfiguration()); + assertThat(manager.getImportConfiguration().getPersist()) + .isEqualTo(org.entando.entando.keycloak.services.mapping.PersistKind.FULL); + } + private Authorization authorization(final String groupName, final String roleName) { final Group group = new Group(); group.setName(groupName); @@ -1459,6 +1609,7 @@ void testCleanSyncDataDisabled() throws Exception { + " realm_access.roles" + " ROLEGROUPCLAIM" + " _SEP_" + + " ignore" + " " + "" + "" @@ -1600,6 +1751,222 @@ void testCleanSyncDataDisabled() throws Exception { + " " + ""; + // ------------------------------------------------------------------------- + // excludeUsers tests + // ------------------------------------------------------------------------- + + @Test + void testExcludeUsersSkipsDynamicSync() throws Exception { + String xml = "" + + " FULL" + + " true" + + "" + + " " + + " true" + + " realm_access.roles" + + " ROLECLAIM" + + " " + + "" + + "" + + " service-account" + + "" + + "generico" + + ""; + + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(xml); + when(userDetails.getUsername()).thenReturn("service-account"); + lenient().when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + + manager.init(); + manager.processNewUser(userDetails, JWT, false); + + verify(authorizationManager, never()).externalAuthSync(anyString(), anyLong(), anyList(), anyList()); + verify(userDetails, never()).addAuthorizations(anyList()); + } + + // ------------------------------------------------------------------------- + // fallback tests — rolegroupclaim (JWT path) + // ------------------------------------------------------------------------- + + private static final String JWT_SINGLE_TOKEN = + "{ \"iat\":1768319143, \"realm_access\":{ \"roles\":[\"singletoken\"] } }"; + + @Test + void testFallbackDefaultTreatsTokenAsRole() throws Exception { + // No → default → treat as role + INFO log + String xml = "" + + " NONE" + + " true" + + "" + + " " + + " true" + + " realm_access.roles" + + " ROLEGROUPCLAIM" + + " _SEP_" + + " " + + "" + + "singletoken" + + "" + + ""; + + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(xml); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + + manager.init(); + manager.processNewUser(userDetails, JWT_SINGLE_TOKEN, false); + + @SuppressWarnings("unchecked") + ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class); + verify(userDetails, times(1)).addAuthorizations(listCaptor.capture()); + assertThat(listCaptor.getValue()).hasSize(1); + assertThat(listCaptor.getValue().get(0).getRole().getName()).isEqualTo("singletoken"); + assertThat(listCaptor.getValue().get(0).getGroup()).isNull(); + } + + @Test + void testFallbackRoleTreatsTokenAsRole() throws Exception { + String xml = "" + + " NONE" + + " true" + + "" + + " " + + " true" + + " realm_access.roles" + + " ROLEGROUPCLAIM" + + " _SEP_" + + " role" + + " " + + "" + + "singletoken" + + "" + + ""; + + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(xml); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + + manager.init(); + manager.processNewUser(userDetails, JWT_SINGLE_TOKEN, false); + + @SuppressWarnings("unchecked") + ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class); + verify(userDetails, times(1)).addAuthorizations(listCaptor.capture()); + assertThat(listCaptor.getValue()).hasSize(1); + assertThat(listCaptor.getValue().get(0).getRole().getName()).isEqualTo("singletoken"); + assertThat(listCaptor.getValue().get(0).getGroup()).isNull(); + } + + @Test + void testFallbackGroupTreatsTokenAsGroup() throws Exception { + String xml = "" + + " NONE" + + " true" + + "" + + " " + + " true" + + " realm_access.roles" + + " ROLEGROUPCLAIM" + + " _SEP_" + + " group" + + " " + + "" + + "" + + "singletoken" + + ""; + + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(xml); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + + manager.init(); + manager.processNewUser(userDetails, JWT_SINGLE_TOKEN, false); + + @SuppressWarnings("unchecked") + ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class); + verify(userDetails, times(1)).addAuthorizations(listCaptor.capture()); + assertThat(listCaptor.getValue()).hasSize(1); + assertThat(listCaptor.getValue().get(0).getGroup().getName()).isEqualTo("singletoken"); + assertThat(listCaptor.getValue().get(0).getRole()).isNull(); + } + + @Test + void testFallbackIgnoreDiscardsToken() throws Exception { + String xml = "" + + " NONE" + + " true" + + "" + + " " + + " true" + + " realm_access.roles" + + " ROLEGROUPCLAIM" + + " _SEP_" + + " ignore" + + " " + + "" + + "singletoken" + + "" + + ""; + + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(xml); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + + manager.init(); + manager.processNewUser(userDetails, JWT_SINGLE_TOKEN, false); + + @SuppressWarnings("unchecked") + ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class); + verify(userDetails, times(1)).addAuthorizations(listCaptor.capture()); + assertThat(listCaptor.getValue()).isEmpty(); + } + + // ------------------------------------------------------------------------- + // fallback test — rolegroup (profile path) + // ------------------------------------------------------------------------- + + @Test + void testFallbackDefaultProfileRoleGroup() throws Exception { + String xml = "" + + " NONE" + + " true" + + "" + + " " + + " true" + + " AD_GROUPROLE" + + " ROLEGROUP" + + " _r_" + + " " + + "" + + "singletoken" + + "" + + ""; + + when(configuration.getDefaultAuthorizations()).thenReturn(null); + when(configManager.getConfigItem(anyString())).thenReturn(xml); + + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("singletoken"))); + when(userDetails.getUserRepresentation()).thenReturn(userRepresentation); + when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>()); + + manager.init(); + manager.processNewUser(userDetails, null, false); + + @SuppressWarnings("unchecked") + ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class); + verify(userDetails, times(1)).addAuthorizations(listCaptor.capture()); + assertThat(listCaptor.getValue()).hasSize(1); + assertThat(listCaptor.getValue().get(0).getRole()).isNotNull(); + assertThat(listCaptor.getValue().get(0).getRole().getName()).isEqualTo("singletoken"); + assertThat(listCaptor.getValue().get(0).getGroup()).isNull(); + } + private static final String JWT_NO_ROLE = "{" + " \"header\" : {" + " \"alg\" : \"RS256\","