mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-18 18:37:54 -05:00
Cache evaluation of client roles with dots for role mapper
Closes #43726 Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
This commit is contained in:
parent
d04d833ec5
commit
7e00961ee1
17 changed files with 368 additions and 169 deletions
|
|
@ -124,7 +124,7 @@ public class HardcodedLDAPRoleStorageMapper extends AbstractLDAPStorageMapper {
|
|||
|
||||
private RoleModel getRole(RealmModel realm) {
|
||||
String roleName = mapperModel.getConfig().getFirst(HardcodedLDAPRoleStorageMapper.ROLE);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(ldapProvider.getSession(), realm, roleName);
|
||||
if (role == null) {
|
||||
logger.warnf("Hardcoded role '%s' configured in mapper '%s' is not available anymore");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,16 @@ package org.keycloak.storage.ldap.mappers;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.cache.AlternativeLookupProvider;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||
|
||||
|
|
@ -35,7 +38,7 @@ import org.keycloak.storage.ldap.LDAPStorageProvider;
|
|||
public class HardcodedLDAPRoleStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "hardcoded-ldap-role-mapper";
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty roleAttr = createConfigProperty(HardcodedLDAPRoleStorageMapper.ROLE,
|
||||
|
|
@ -62,13 +65,18 @@ public class HardcodedLDAPRoleStorageMapperFactory extends AbstractLDAPStorageMa
|
|||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends Provider>> dependsOn() {
|
||||
return Set.of(AlternativeLookupProvider.class); // for caching
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
|
||||
String roleName = config.getConfig().getFirst(HardcodedLDAPRoleStorageMapper.ROLE);
|
||||
if (roleName == null) {
|
||||
throw new ComponentValidationException("Role can't be null");
|
||||
}
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(session, realm, roleName);
|
||||
if (role == null) {
|
||||
throw new ComponentValidationException("There is no role corresponding to configured value");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,16 @@
|
|||
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.broker.provider.mappersync.ConfigSyncEventListener;
|
||||
import org.keycloak.cache.AlternativeLookupProvider;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
|
|
@ -92,4 +96,9 @@ public abstract class AbstractIdentityProviderMapper implements IdentityProvider
|
|||
public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
updateBrokeredUser(session, realm, user, mapperModel, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends Provider>> dependsOn() {
|
||||
return Set.of(AlternativeLookupProvider.class); //for caching
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import java.util.Map;
|
|||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
public interface AlternativeLookupProvider extends Provider {
|
||||
|
|
@ -13,4 +15,30 @@ public interface AlternativeLookupProvider extends Provider {
|
|||
|
||||
ClientModel lookupClientFromClientAttributes(KeycloakSession session, Map<String, String> attributes);
|
||||
|
||||
/**
|
||||
* Looks up a role from its string representation, supporting both realm and client roles.
|
||||
* <p>
|
||||
* The method interprets the {@code roleName} parameter as follows:
|
||||
* <ul>
|
||||
* <li>For realm roles: the role name directly (e.g., {@code "admin"})</li>
|
||||
* <li>For client roles: the format {@code "client-id.role-name"} where the client ID and role name
|
||||
* are separated by a dot separator</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Since client IDs can contain dots, the method attempts multiple splits from right to left to resolve ambiguous
|
||||
* role names. For example, {@code "my.client.app.role"} will first try to look up
|
||||
* client {@code "my.client.app"} with role {@code "role"}, then client {@code "my.client"} with role
|
||||
* {@code "app.role"}, and so on.
|
||||
* <p>
|
||||
* The lookup uses caching to reduce database load. If a role is not found in the cache, the method
|
||||
* performs a database lookup and caches the result for subsequent calls.
|
||||
*
|
||||
* @param realm the realm in which to look up the role
|
||||
* @param roleName the string representation of the role name, which can be a realm role name or a client role in
|
||||
* the format {@code "client-id.role-name"}. May be {@code null}.
|
||||
* @return the corresponding {@link RoleModel} if found, or {@code null} if the role does not exist or if
|
||||
* {@code roleName} is {@code null}
|
||||
*/
|
||||
RoleModel lookupRoleFromString(RealmModel realm, String roleName);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import org.keycloak.Config;
|
|||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||
import org.keycloak.broker.social.SocialIdentityProviderFactory;
|
||||
import org.keycloak.cache.AlternativeLookupProvider;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
|
|
@ -109,7 +110,7 @@ public final class KeycloakModelUtils {
|
|||
|
||||
public static final String GROUP_PATH_SEPARATOR = "/";
|
||||
public static final String GROUP_PATH_ESCAPE = "~";
|
||||
private static final char CLIENT_ROLE_SEPARATOR = '.';
|
||||
public static final char CLIENT_ROLE_SEPARATOR = '.';
|
||||
|
||||
public static final int MAX_CLIENT_LOOKUPS_DURING_ROLE_RESOLVE = 25;
|
||||
|
||||
|
|
@ -695,7 +696,7 @@ public final class KeycloakModelUtils {
|
|||
|
||||
public static Collection<String> resolveAttribute(UserModel user, String name, boolean aggregateAttrs) {
|
||||
List<String> values = user.getAttributeStream(name).collect(Collectors.toList());
|
||||
Set<String> aggrValues = new HashSet<String>();
|
||||
Set<String> aggrValues = new HashSet<>();
|
||||
if (!values.isEmpty()) {
|
||||
if (!aggregateAttrs) {
|
||||
return values;
|
||||
|
|
@ -717,70 +718,13 @@ public final class KeycloakModelUtils {
|
|||
}
|
||||
|
||||
|
||||
private static GroupModel findSubGroup(String[] segments, int index, GroupModel parent) {
|
||||
return parent.getSubGroupsStream().map(group -> {
|
||||
String groupName = group.getName();
|
||||
String[] pathSegments = formatPathSegments(segments, index, groupName);
|
||||
|
||||
if (groupName.equals(pathSegments[index])) {
|
||||
if (pathSegments.length == index + 1) {
|
||||
return group;
|
||||
} else {
|
||||
if (index + 1 < pathSegments.length) {
|
||||
GroupModel found = findSubGroup(pathSegments, index + 1, group);
|
||||
if (found != null) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}).filter(Objects::nonNull).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the {@code pathParts} of a group with the given {@code groupName}, format the {@code segments} in order to ignore
|
||||
* group names containing a {@code /} character.
|
||||
*
|
||||
* @param segments the path segments
|
||||
* @param index the index pointing to the position to start looking for the group name
|
||||
* @param groupName the groupName
|
||||
* @return a new array of strings with the correct segments in case the group has a name containing slashes
|
||||
*/
|
||||
private static String[] formatPathSegments(String[] segments, int index, String groupName) {
|
||||
String[] nameSegments = groupName.split(GROUP_PATH_SEPARATOR);
|
||||
|
||||
if (nameSegments.length > 1 && segments.length >= nameSegments.length) {
|
||||
for (int i = 0; i < nameSegments.length; i++) {
|
||||
if (!nameSegments[i].equals(segments[index + i])) {
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
|
||||
int numMergedIndexes = nameSegments.length - 1;
|
||||
String[] newPath = new String[segments.length - numMergedIndexes];
|
||||
|
||||
for (int i = 0; i < newPath.length; i++) {
|
||||
if (i == index) {
|
||||
newPath[i] = groupName;
|
||||
} else if (i > index) {
|
||||
newPath[i] = segments[i + numMergedIndexes];
|
||||
} else {
|
||||
newPath[i] = segments[i];
|
||||
}
|
||||
}
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get from the session if group path slashes should be escaped or not.
|
||||
* @param session The session
|
||||
* @return true or false
|
||||
*/
|
||||
public static boolean escapeSlashesInGroupPath(KeycloakSession session) {
|
||||
GroupProviderFactory fact = (GroupProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(GroupProvider.class);
|
||||
GroupProviderFactory<?> fact = (GroupProviderFactory<?>) session.getKeycloakSessionFactory().getProviderFactory(GroupProvider.class);
|
||||
return fact.escapeSlashesInGroupPath();
|
||||
}
|
||||
|
||||
|
|
@ -993,8 +937,24 @@ public final class KeycloakModelUtils {
|
|||
Objects.equals(client.getId(), role.getContainer().getId()));
|
||||
}
|
||||
|
||||
// Used in various role mappers
|
||||
/**
|
||||
* @deprecated for removal. Use {@link #getRoleFromString(KeycloakSession, RealmModel, String)} instead.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "26.6")
|
||||
public static RoleModel getRoleFromString(RealmModel realm, String roleName) {
|
||||
return getRoleFromString(KeycloakSessionUtil.getKeycloakSession(), realm, roleName);
|
||||
}
|
||||
|
||||
public static RoleModel getRoleFromString(KeycloakSession session, RealmModel realm, String roleName) {
|
||||
if (session == null) {
|
||||
return getRoleFromStringNoCaching(realm, roleName);
|
||||
}
|
||||
return session.getProvider(AlternativeLookupProvider.class)
|
||||
.lookupRoleFromString(realm, roleName);
|
||||
}
|
||||
|
||||
// Used in various role mappers
|
||||
private static RoleModel getRoleFromStringNoCaching(RealmModel realm, String roleName) {
|
||||
if (roleName == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1028,11 +988,9 @@ public final class KeycloakModelUtils {
|
|||
if (scopeIndex > -1) {
|
||||
String appName = role.substring(0, scopeIndex);
|
||||
role = role.substring(scopeIndex + 1);
|
||||
String[] rtn = {appName, role};
|
||||
return rtn;
|
||||
return new String[]{appName, role};
|
||||
} else {
|
||||
String[] rtn = {null, role};
|
||||
return rtn;
|
||||
return new String[]{null, role};
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1222,7 +1180,7 @@ public final class KeycloakModelUtils {
|
|||
return displayName;
|
||||
}
|
||||
|
||||
SocialIdentityProviderFactory providerFactory = (SocialIdentityProviderFactory) session.getKeycloakSessionFactory()
|
||||
SocialIdentityProviderFactory<?> providerFactory = (SocialIdentityProviderFactory<?>) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(SocialIdentityProvider.class, provider.getProviderId());
|
||||
if (providerFactory != null) {
|
||||
return providerFactory.getName();
|
||||
|
|
@ -1258,7 +1216,7 @@ public final class KeycloakModelUtils {
|
|||
* @throws RuntimeException if a group does not exist
|
||||
*/
|
||||
public static void setDefaultGroups(KeycloakSession session, RealmModel realm, Stream<String> groups) {
|
||||
realm.getDefaultGroupsStream().collect(Collectors.toList()).forEach(realm::removeDefaultGroup);
|
||||
realm.getDefaultGroupsStream().toList().forEach(realm::removeDefaultGroup);
|
||||
groups.forEach(path -> {
|
||||
GroupModel found = KeycloakModelUtils.findGroupByPath(session, realm, path);
|
||||
if (found == null) throw new RuntimeException("default group in realm rep doesn't exist: " + path);
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ public class KeycloakModelUtilsTest {
|
|||
}
|
||||
Assert.assertEquals(65536, badRoleName.length());
|
||||
|
||||
Assert.assertNull(KeycloakModelUtils.getRoleFromString(realm, badRoleName));
|
||||
Assert.assertNull(KeycloakModelUtils.getRoleFromString(null, realm, badRoleName));
|
||||
Assert.assertEquals(KeycloakModelUtils.MAX_CLIENT_LOOKUPS_DURING_ROLE_RESOLVE, counter.get());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ package org.keycloak.authentication.authenticators.browser;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
|
||||
|
|
@ -117,7 +115,7 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
return;
|
||||
}
|
||||
|
||||
if (tryConcludeBasedOn(voteForUserRole(context.getRealm(), context.getUser(), config), context)) {
|
||||
if (tryConcludeBasedOn(voteForUserRole(context.getSession(), context.getRealm(), context.getUser(), config), context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -138,46 +136,29 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
return ABSTAIN;
|
||||
}
|
||||
|
||||
switch (config.get(DEFAULT_OTP_OUTCOME)) {
|
||||
case SKIP:
|
||||
return SKIP_OTP;
|
||||
case FORCE:
|
||||
return SHOW_OTP;
|
||||
default:
|
||||
return ABSTAIN;
|
||||
}
|
||||
return switch (config.get(DEFAULT_OTP_OUTCOME)) {
|
||||
case SKIP -> SKIP_OTP;
|
||||
case FORCE -> SHOW_OTP;
|
||||
default -> ABSTAIN;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean tryConcludeBasedOn(OtpDecision state, AuthenticationFlowContext context) {
|
||||
|
||||
switch (state) {
|
||||
|
||||
case SHOW_OTP:
|
||||
return switch (state) {
|
||||
case SHOW_OTP -> {
|
||||
showOtpForm(context);
|
||||
return true;
|
||||
|
||||
case SKIP_OTP:
|
||||
yield true;
|
||||
}
|
||||
case SKIP_OTP -> {
|
||||
context.success();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean tryConcludeBasedOn(OtpDecision state) {
|
||||
|
||||
switch (state) {
|
||||
|
||||
case SHOW_OTP:
|
||||
return true;
|
||||
|
||||
case SKIP_OTP:
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
private static boolean tryConcludeBasedOn(OtpDecision state) {
|
||||
return state == SHOW_OTP;
|
||||
}
|
||||
|
||||
private void showOtpForm(AuthenticationFlowContext context) {
|
||||
|
|
@ -195,19 +176,13 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
return ABSTAIN;
|
||||
}
|
||||
|
||||
Optional<String> value = user.getAttributeStream(attributeName).findFirst();
|
||||
if (!value.isPresent()) {
|
||||
return ABSTAIN;
|
||||
}
|
||||
|
||||
switch (value.get().trim()) {
|
||||
case SKIP:
|
||||
return SKIP_OTP;
|
||||
case FORCE:
|
||||
return SHOW_OTP;
|
||||
default:
|
||||
return ABSTAIN;
|
||||
}
|
||||
return user.getAttributeStream(attributeName)
|
||||
.findFirst()
|
||||
.map(s -> switch (s.trim()) {
|
||||
case SKIP -> SKIP_OTP;
|
||||
case FORCE -> SHOW_OTP;
|
||||
default -> ABSTAIN;
|
||||
}).orElse(ABSTAIN);
|
||||
}
|
||||
|
||||
private OtpDecision voteForHttpHeaderMatchesPattern(MultivaluedMap<String, String> requestHeaders, Map<String, String> config) {
|
||||
|
|
@ -257,30 +232,30 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
return false;
|
||||
}
|
||||
|
||||
private OtpDecision voteForUserRole(RealmModel realm, UserModel user, Map<String, String> config) {
|
||||
private OtpDecision voteForUserRole(KeycloakSession session, RealmModel realm, UserModel user, Map<String, String> config) {
|
||||
|
||||
if (!config.containsKey(SKIP_OTP_ROLE) && !config.containsKey(FORCE_OTP_ROLE)) {
|
||||
return ABSTAIN;
|
||||
}
|
||||
|
||||
if (userHasRole(realm, user, config.get(SKIP_OTP_ROLE))) {
|
||||
if (userHasRole(session, realm, user, config.get(SKIP_OTP_ROLE))) {
|
||||
return SKIP_OTP;
|
||||
}
|
||||
|
||||
if (userHasRole(realm, user, config.get(FORCE_OTP_ROLE))) {
|
||||
if (userHasRole(session, realm, user, config.get(FORCE_OTP_ROLE))) {
|
||||
return SHOW_OTP;
|
||||
}
|
||||
|
||||
return ABSTAIN;
|
||||
}
|
||||
|
||||
private boolean userHasRole(RealmModel realm, UserModel user, String roleName) {
|
||||
private boolean userHasRole(KeycloakSession session, RealmModel realm, UserModel user, String roleName) {
|
||||
|
||||
if (roleName == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RoleModel role = getRoleFromString(realm, roleName);
|
||||
RoleModel role = getRoleFromString(session, realm, roleName);
|
||||
if (role != null) {
|
||||
return user.hasRole(role);
|
||||
}
|
||||
|
|
@ -290,8 +265,8 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
private boolean isOTPRequired(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
MultivaluedMap<String, String> requestHeaders = session.getContext().getRequestHeaders().getRequestHeaders();
|
||||
List<Map<String,String>> configs = realm.getAuthenticatorConfigsStream().map(AuthenticatorConfigModel::getConfig)
|
||||
.filter(this::containsConditionalOtpConfig)
|
||||
.collect(Collectors.toList());
|
||||
.filter(ConditionalOtpFormAuthenticator::containsConditionalOtpConfig)
|
||||
.toList();
|
||||
if (configs.isEmpty()) {
|
||||
// no configuration at all means it is configured
|
||||
return true;
|
||||
|
|
@ -300,7 +275,7 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
if (tryConcludeBasedOn(voteForUserOtpControlAttribute(user, config))) {
|
||||
return true;
|
||||
}
|
||||
if (tryConcludeBasedOn(voteForUserRole(realm, user, config))) {
|
||||
if (tryConcludeBasedOn(voteForUserRole(session, realm, user, config))) {
|
||||
return true;
|
||||
}
|
||||
if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(requestHeaders, config))) {
|
||||
|
|
@ -312,13 +287,13 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
return true;
|
||||
}
|
||||
return voteForUserOtpControlAttribute(user, config) == ABSTAIN
|
||||
&& voteForUserRole(realm, user, config) == ABSTAIN
|
||||
&& voteForUserRole(session, realm, user, config) == ABSTAIN
|
||||
&& voteForHttpHeaderMatchesPattern(requestHeaders, config) == ABSTAIN
|
||||
&& (voteForDefaultFallback(config) == SHOW_OTP || voteForDefaultFallback(config) == ABSTAIN);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean containsConditionalOtpConfig(Map config) {
|
||||
private static boolean containsConditionalOtpConfig(Map<?,?> config) {
|
||||
return config.containsKey(OTP_CONTROL_USER_ATTRIBUTE)
|
||||
|| config.containsKey(SKIP_OTP_ROLE)
|
||||
|| config.containsKey(FORCE_OTP_ROLE)
|
||||
|
|
|
|||
|
|
@ -18,14 +18,17 @@
|
|||
package org.keycloak.authentication.authenticators.browser;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.cache.AlternativeLookupProvider;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
|
@ -157,4 +160,9 @@ public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFact
|
|||
|
||||
return asList(forceOtpUserAttribute, skipOtpRole, forceOtpRole, skipOtpForHttpHeader, forceOtpForHttpHeader, defaultOutcome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends Provider>> dependsOn() {
|
||||
return Set.of(AlternativeLookupProvider.class); //for caching
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public class ConditionalRoleAuthenticator implements ConditionalAuthenticator {
|
|||
if (user != null && authConfig!=null && authConfig.getConfig()!=null) {
|
||||
String requiredRole = authConfig.getConfig().get(ConditionalRoleAuthenticatorFactory.CONDITIONAL_USER_ROLE);
|
||||
boolean negateOutput = Boolean.parseBoolean(authConfig.getConfig().get(ConditionalRoleAuthenticatorFactory.CONF_NEGATE));
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, requiredRole);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(context.getSession(), realm, requiredRole);
|
||||
if (role == null) {
|
||||
logger.errorv("Invalid role name submitted: {0}", requiredRole);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public abstract class AbstractClaimToRoleMapper extends AbstractClaimMapper {
|
|||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = getRole(realm, mapperModel);
|
||||
RoleModel role = getRole(session, realm, mapperModel);
|
||||
if (role == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ public abstract class AbstractClaimToRoleMapper extends AbstractClaimMapper {
|
|||
|
||||
@Override
|
||||
public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = getRole(realm, mapperModel);
|
||||
RoleModel role = getRole(session, realm, mapperModel);
|
||||
if (role == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ public abstract class AbstractClaimToRoleMapper extends AbstractClaimMapper {
|
|||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = getRole(realm, mapperModel);
|
||||
RoleModel role = getRole(session, realm, mapperModel);
|
||||
if (role == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -94,17 +94,17 @@ public abstract class AbstractClaimToRoleMapper extends AbstractClaimMapper {
|
|||
|
||||
/**
|
||||
* Obtains the {@link RoleModel} corresponding the role configured in the specified
|
||||
* {@link IdentityProviderMapperModel}.
|
||||
* If the role doesn't correspond to one of the realm's client roles or to one of the realm's roles, this method
|
||||
* returns {@code null}.
|
||||
* {@link IdentityProviderMapperModel}. If the role doesn't correspond to one of the realm's client roles or to one
|
||||
* of the realm's roles, this method returns {@code null}.
|
||||
*
|
||||
* @param realm a reference to the realm.
|
||||
* @param session the {@link KeycloakSession}.
|
||||
* @param realm a reference to the realm.
|
||||
* @param mapperModel a reference to the {@link IdentityProviderMapperModel} containing the configured role.
|
||||
* @return the {@link RoleModel} that corresponds to the mapper model role; {@code null}, when role was not found
|
||||
*/
|
||||
private RoleModel getRole(final RealmModel realm, final IdentityProviderMapperModel mapperModel) {
|
||||
private RoleModel getRole(KeycloakSession session, final RealmModel realm, final IdentityProviderMapperModel mapperModel) {
|
||||
String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(session, realm, roleName);
|
||||
|
||||
if (role == null) {
|
||||
LOG.warnf("Unable to find role '%s' referenced by mapper '%s' on realm '%s'.", roleName,
|
||||
|
|
|
|||
|
|
@ -93,19 +93,19 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper {
|
|||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
grantUserRole(realm, user, mapperModel);
|
||||
grantUserRole(session, realm, user, mapperModel);
|
||||
}
|
||||
|
||||
private void grantUserRole(RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel) {
|
||||
RoleModel role = getRole(realm, mapperModel);
|
||||
private void grantUserRole(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel) {
|
||||
RoleModel role = getRole(session, realm, mapperModel);
|
||||
if (role != null) {
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
private RoleModel getRole(final RealmModel realm, final IdentityProviderMapperModel mapperModel) {
|
||||
private RoleModel getRole(KeycloakSession session, final RealmModel realm, final IdentityProviderMapperModel mapperModel) {
|
||||
String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(session, realm, roleName);
|
||||
|
||||
if (role == null) {
|
||||
LOG.warnf("Unable to find role '%s' referenced by mapper '%s' on realm '%s'.", roleName,
|
||||
|
|
@ -117,7 +117,7 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper {
|
|||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
grantUserRole(realm, user, mapperModel);
|
||||
grantUserRole(session, realm, user, mapperModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public abstract class AbstractAttributeToRoleMapper extends AbstractIdentityProv
|
|||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = this.getRole(realm, mapperModel);
|
||||
RoleModel role = this.getRole(session, realm, mapperModel);
|
||||
if (role == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ public abstract class AbstractAttributeToRoleMapper extends AbstractIdentityProv
|
|||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = this.getRole(realm, mapperModel);
|
||||
RoleModel role = this.getRole(session, realm, mapperModel);
|
||||
if (role == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -88,18 +88,18 @@ public abstract class AbstractAttributeToRoleMapper extends AbstractIdentityProv
|
|||
|
||||
/**
|
||||
* Obtains the {@link RoleModel} corresponding the role configured in the specified
|
||||
* {@link IdentityProviderMapperModel}.
|
||||
* If the role doesn't correspond to one of the realm's client roles or to one of the realm's roles, this method
|
||||
* returns {@code null}.
|
||||
* {@link IdentityProviderMapperModel}. If the role doesn't correspond to one of the realm's client roles or to one
|
||||
* of the realm's roles, this method returns {@code null}.
|
||||
*
|
||||
* @param realm a reference to the realm.
|
||||
* @param session the {@link KeycloakSession}.
|
||||
* @param realm a reference to the realm.
|
||||
* @param mapperModel a reference to the {@link IdentityProviderMapperModel} containing the configured role.
|
||||
* @return the {@link RoleModel} that corresponds to the mapper model role or {@code null}, if the role could not be
|
||||
* found
|
||||
* found
|
||||
*/
|
||||
private RoleModel getRole(final RealmModel realm, final IdentityProviderMapperModel mapperModel) {
|
||||
private RoleModel getRole(KeycloakSession session, final RealmModel realm, final IdentityProviderMapperModel mapperModel) {
|
||||
String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(session, realm, roleName);
|
||||
if (role == null) {
|
||||
LOG.warnf("Unable to find role '%s' for mapper '%s' on realm '%s'.", roleName, mapperModel.getName(),
|
||||
realm.getName());
|
||||
|
|
|
|||
96
services/src/main/java/org/keycloak/cache/CachedValue.java
vendored
Normal file
96
services/src/main/java/org/keycloak/cache/CachedValue.java
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright 2026 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.cache;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Internal representation of cached lookup values.
|
||||
* <p>
|
||||
* This is an implementation detail used by {@link DefaultAlternativeLookupProvider} to cache alternative lookups and
|
||||
* reduce database load. The interface provides type-safe wrappers for different kinds of cached values used in the
|
||||
* lookup cache.
|
||||
*/
|
||||
interface CachedValue {
|
||||
|
||||
/**
|
||||
* Creates a cached value wrapping a simple identifier string.
|
||||
*
|
||||
* @param value the non-null identifier value to cache
|
||||
* @return a cached string value
|
||||
*/
|
||||
static CachedString ofId(String value) {
|
||||
return new CachedString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cached value for a client role lookup.
|
||||
*
|
||||
* @param clientId the non-null client identifier
|
||||
* @param roleName the non-null role name
|
||||
* @return a cached role qualifier for a client role
|
||||
*/
|
||||
static CachedRoleQualifier ofClientRole(String clientId, String roleName) {
|
||||
return new CachedRoleQualifier(Objects.requireNonNull(clientId), roleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cached value for a realm role lookup.
|
||||
*
|
||||
* @param roleName the non-null role name
|
||||
* @return a cached role qualifier for a realm role
|
||||
*/
|
||||
static CachedRoleQualifier ofRealmRole(String roleName) {
|
||||
return new CachedRoleQualifier(null, roleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* A cached value wrapping a simple string identifier.
|
||||
*
|
||||
* @param value the non-null identifier value
|
||||
*/
|
||||
record CachedString(String value) implements CachedValue {
|
||||
public CachedString {
|
||||
Objects.requireNonNull(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A cached value wrapping role qualifier information.
|
||||
* <p>
|
||||
* For client roles, both {@code clientId} and {@code roleName} are present. For realm roles, {@code clientId} is
|
||||
* {@code null}.
|
||||
*
|
||||
* @param clientId the client identifier, or {@code null} for realm roles
|
||||
* @param roleName the non-null role name
|
||||
*/
|
||||
record CachedRoleQualifier(String clientId, String roleName) implements CachedValue {
|
||||
public CachedRoleQualifier {
|
||||
Objects.requireNonNull(roleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this qualifier represents a realm role.
|
||||
*
|
||||
* @return {@code true} if this is a realm role (clientId is null), {@code false} otherwise
|
||||
*/
|
||||
public boolean isRealmRole() {
|
||||
return clientId == null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,21 +7,30 @@ import org.keycloak.models.ClientModel;
|
|||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IdentityProviderQuery;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import static org.keycloak.models.utils.KeycloakModelUtils.CLIENT_ROLE_SEPARATOR;
|
||||
import static org.keycloak.models.utils.KeycloakModelUtils.MAX_CLIENT_LOOKUPS_DURING_ROLE_RESOLVE;
|
||||
|
||||
public class DefaultAlternativeLookupProvider implements AlternativeLookupProvider {
|
||||
|
||||
private final LocalCache<String, String> lookupCache;
|
||||
private static final Logger logger = Logger.getLogger(DefaultAlternativeLookupProvider.class);
|
||||
private final LocalCache<String, CachedValue> lookupCache;
|
||||
|
||||
public DefaultAlternativeLookupProvider(LocalCache<String, String> lookupCache) {
|
||||
DefaultAlternativeLookupProvider(LocalCache<String, CachedValue> lookupCache) {
|
||||
this.lookupCache = lookupCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderModel lookupIdentityProviderFromIssuer(KeycloakSession session, String issuerUrl) {
|
||||
String alternativeKey = ComputedKey.computeKey(session.getContext().getRealm().getId(), "idp", issuerUrl);
|
||||
|
||||
String cachedIdpAlias = lookupCache.get(alternativeKey);
|
||||
if (cachedIdpAlias != null) {
|
||||
IdentityProviderModel idp = session.identityProviders().getByAlias(cachedIdpAlias);
|
||||
CachedValue cachedIdpAlias = lookupCache.get(alternativeKey);
|
||||
if (cachedIdpAlias instanceof CachedValue.CachedString cachedString) {
|
||||
IdentityProviderModel idp = session.identityProviders().getByAlias(cachedString.value());
|
||||
if (idp != null && issuerUrl.equals(idp.getConfig().get(IdentityProviderModel.ISSUER))) {
|
||||
return idp;
|
||||
} else {
|
||||
|
|
@ -37,7 +46,7 @@ public class DefaultAlternativeLookupProvider implements AlternativeLookupProvid
|
|||
if (idps.size() == 1) {
|
||||
idp = idps.get(0);
|
||||
if (idp.getAlias() != null) {
|
||||
lookupCache.put(alternativeKey, idp.getAlias());
|
||||
lookupCache.put(alternativeKey, CachedValue.ofId(idp.getAlias()));
|
||||
}
|
||||
} else if (idps.size() > 1) {
|
||||
throw new RuntimeException("Multiple IDPs match the same issuer: " + idps.stream().map(IdentityProviderModel::getAlias).toList());
|
||||
|
|
@ -46,12 +55,13 @@ public class DefaultAlternativeLookupProvider implements AlternativeLookupProvid
|
|||
return idp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel lookupClientFromClientAttributes(KeycloakSession session, Map<String, String> attributes) {
|
||||
String alternativeKey = ComputedKey.computeKey(session.getContext().getRealm().getId(), "client", attributes);
|
||||
|
||||
String cachedClientId = lookupCache.get(alternativeKey);
|
||||
if (cachedClientId != null) {
|
||||
ClientModel client = session.clients().getClientByClientId(session.getContext().getRealm(), cachedClientId);
|
||||
CachedValue cachedClientId = lookupCache.get(alternativeKey);
|
||||
if (cachedClientId instanceof CachedValue.CachedString cachedString) {
|
||||
ClientModel client = session.clients().getClientByClientId(session.getContext().getRealm(), cachedString.value());
|
||||
boolean match = client != null;
|
||||
if (match) {
|
||||
for (Map.Entry<String, String> e : attributes.entrySet()) {
|
||||
|
|
@ -72,7 +82,7 @@ public class DefaultAlternativeLookupProvider implements AlternativeLookupProvid
|
|||
List<ClientModel> clients = session.clients().searchClientsByAttributes(session.getContext().getRealm(), attributes, 0, 2).toList();
|
||||
if (clients.size() == 1) {
|
||||
client = clients.get(0);
|
||||
lookupCache.put(alternativeKey, client.getClientId());
|
||||
lookupCache.put(alternativeKey, CachedValue.ofId(client.getClientId()));
|
||||
} else if (clients.size() > 1) {
|
||||
throw new RuntimeException("Multiple clients matches attributes");
|
||||
}
|
||||
|
|
@ -80,7 +90,86 @@ public class DefaultAlternativeLookupProvider implements AlternativeLookupProvid
|
|||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RoleModel lookupRoleFromString(RealmModel realm, String roleName) {
|
||||
if (roleName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var roleModel = findRoleInCache(realm, roleName);
|
||||
if (roleModel != null) {
|
||||
return roleModel;
|
||||
}
|
||||
|
||||
// Check client roles for all possible splits by dot
|
||||
int counter = 0;
|
||||
int scopeIndex = roleName.lastIndexOf(CLIENT_ROLE_SEPARATOR);
|
||||
while (scopeIndex >= 0 && counter < MAX_CLIENT_LOOKUPS_DURING_ROLE_RESOLVE) {
|
||||
counter++;
|
||||
String appName = roleName.substring(0, scopeIndex);
|
||||
ClientModel client = realm.getClientByClientId(appName);
|
||||
if (client != null) {
|
||||
return storeClientRoleInCache(client, roleName, roleName.substring(scopeIndex + 1), counter);
|
||||
}
|
||||
|
||||
scopeIndex = roleName.lastIndexOf(CLIENT_ROLE_SEPARATOR, scopeIndex - 1);
|
||||
}
|
||||
if (counter >= MAX_CLIENT_LOOKUPS_DURING_ROLE_RESOLVE) {
|
||||
logger.warnf("Not able to retrieve role model from the role name '%s'. Please use shorter role names with the limited amount of dots, roleName", roleName.length() > 100 ? roleName.substring(0, 100) + "..." : roleName);
|
||||
return null;
|
||||
}
|
||||
|
||||
return storeRealmRoleInCache(realm, roleName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
private RoleModel findRoleInCache(RealmModel realm, String roleName) {
|
||||
var cachedRole = lookupCache.get(roleName);
|
||||
if (!(cachedRole instanceof CachedValue.CachedRoleQualifier cachedRoleQualifier)) {
|
||||
return null;
|
||||
}
|
||||
if (cachedRoleQualifier.isRealmRole()) {
|
||||
var role = realm.getRole(cachedRoleQualifier.roleName());
|
||||
if (role == null) {
|
||||
lookupCache.invalidate(roleName);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
var client = realm.getClientByClientId(cachedRoleQualifier.clientId());
|
||||
if (client == null) {
|
||||
lookupCache.invalidate(roleName);
|
||||
return null;
|
||||
}
|
||||
|
||||
var role = client.getRole(cachedRoleQualifier.roleName());
|
||||
if (role == null) {
|
||||
lookupCache.invalidate(roleName);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
private RoleModel storeClientRoleInCache(ClientModel client, String cacheKey, String roleName, int dotCount) {
|
||||
// If dotCount is equals to 1, we skip caching.
|
||||
// It means, we have the following format, client-id.role-name.
|
||||
// Both realm.getClientByClientId and client.getRole methods already use an internal cache.
|
||||
var roleModel = client.getRole(roleName);
|
||||
if (roleModel != null && dotCount > 1) {
|
||||
lookupCache.put(cacheKey, CachedValue.ofClientRole(client.getClientId(), roleName));
|
||||
}
|
||||
return roleModel;
|
||||
}
|
||||
|
||||
private RoleModel storeRealmRoleInCache(RealmModel realm, String roleName) {
|
||||
// determine if roleName is a realm role
|
||||
var roleModel = realm.getRole(roleName);
|
||||
if (roleModel != null) {
|
||||
// only cache if the role is present
|
||||
lookupCache.put(roleName, CachedValue.ofRealmRole(roleName));
|
||||
}
|
||||
return roleModel;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import org.keycloak.models.KeycloakSessionFactory;
|
|||
|
||||
public class DefaultAlternativeLookupProviderFactory implements AlternativeLookupProviderFactory {
|
||||
|
||||
private LocalCacheConfiguration<String, String> cacheConfig;
|
||||
private LocalCache<String, String> lookupCache;
|
||||
private LocalCacheConfiguration<String, CachedValue> cacheConfig;
|
||||
private LocalCache<String, CachedValue> lookupCache;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
|
|
@ -26,7 +26,7 @@ public class DefaultAlternativeLookupProviderFactory implements AlternativeLooku
|
|||
Integer maximumSize = config.getInt("maximumSize", 1000);
|
||||
Integer expireAfter = config.getInt("expireAfter", 60);
|
||||
|
||||
cacheConfig = LocalCacheConfiguration.<String, String>builder()
|
||||
cacheConfig = LocalCacheConfiguration.<String, CachedValue>builder()
|
||||
.name("lookup")
|
||||
.expiration(Duration.ofMinutes(expireAfter))
|
||||
.maxSize(maximumSize)
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ public class ClientUpdaterSourceRolesCondition extends AbstractClientPolicyCondi
|
|||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
for (String roleName : expectedRoles) {
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(session, realm, roleName);
|
||||
if (role == null) continue;
|
||||
if (user.hasRole(role)) return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,15 @@
|
|||
|
||||
package org.keycloak.tests.model;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.keycloak.cache.AlternativeLookupProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.RealmModelDelegate;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
|
|
@ -55,6 +61,28 @@ public class AlternativeLookupProviderTest {
|
|||
}
|
||||
}
|
||||
|
||||
@TestOnServer
|
||||
public void testLimitCountOfClientLookupsDuringGetRoleFromString(KeycloakSession session) {
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
RealmModel realm = new RealmModelDelegate(null) {
|
||||
@Override
|
||||
public ClientModel getClientByClientId(String clientId) {
|
||||
counter.incrementAndGet();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
String badRoleName = ".";
|
||||
for (int i = 0 ; i < 16 ; i++) {
|
||||
badRoleName = badRoleName + badRoleName;
|
||||
}
|
||||
Assertions.assertEquals(65536, badRoleName.length());
|
||||
|
||||
Assertions.assertNull(KeycloakModelUtils.getRoleFromString(session, realm, badRoleName));
|
||||
Assertions.assertEquals(KeycloakModelUtils.MAX_CLIENT_LOOKUPS_DURING_ROLE_RESOLVE, counter.get());
|
||||
}
|
||||
|
||||
|
||||
protected IdentityProviderModel createModel(String alias, String providerId) {
|
||||
return createModel(alias, providerId,true);
|
||||
|
|
|
|||
Loading…
Reference in a new issue