Avoid iterating through all mappers when running the config event listeners

Closes #32233

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2024-09-03 09:35:09 -03:00 committed by Pedro Igor
parent 3274591fe1
commit 557d7e87b2
10 changed files with 131 additions and 152 deletions

View file

@ -239,8 +239,8 @@ public class InfinispanIdentityProviderStorageProvider implements IdentityProvid
} }
@Override @Override
public Stream<IdentityProviderMapperModel> getMappersStream() { public Stream<IdentityProviderMapperModel> getMappersStream(Map<String, String> options, Integer first, Integer max) {
return idpDelegate.getMappersStream(); return idpDelegate.getMappersStream(options, first, max);
} }
@Override @Override

View file

@ -238,7 +238,9 @@ public class JpaIdentityProviderStorageProvider implements IdentityProviderStora
} }
break; break;
} }
case ALIAS:
case FIRST_BROKER_LOGIN_FLOW_ID: case FIRST_BROKER_LOGIN_FLOW_ID:
case POST_BROKER_LOGIN_FLOW_ID:
case ORGANIZATION_ID: { case ORGANIZATION_ID: {
if (StringUtil.isBlank(value)) { if (StringUtil.isBlank(value)) {
predicates.add(builder.isNull(idp.get(key))); predicates.add(builder.isNull(idp.get(key)));
@ -388,16 +390,52 @@ public class JpaIdentityProviderStorageProvider implements IdentityProviderStora
} }
@Override @Override
public Stream<IdentityProviderMapperModel> getMappersStream() { public Stream<IdentityProviderMapperModel> getMappersStream(Map<String, String> options, Integer first, Integer max) {
CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<IdentityProviderMapperEntity> query = builder.createQuery(IdentityProviderMapperEntity.class); CriteriaQuery<IdentityProviderMapperEntity> query = builder.createQuery(IdentityProviderMapperEntity.class);
Root<IdentityProviderMapperEntity> mapper = query.from(IdentityProviderMapperEntity.class); Root<IdentityProviderMapperEntity> idp = query.from(IdentityProviderMapperEntity.class);
Predicate predicate = builder.equal(mapper.get("realmId"), getRealm().getId()); List<Predicate> predicates = new ArrayList<>();
predicates.add(builder.equal(idp.get("realmId"), getRealm().getId()));
TypedQuery<IdentityProviderMapperEntity> typedQuery = em.createQuery(query.select(mapper).where(predicate)); if (options != null) {
for (Map.Entry<String, String> entry : options.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (StringUtil.isBlank(key)) {
continue;
}
String dbProductName = em.unwrap(Session.class).doReturningWork(connection -> connection.getMetaData().getDatabaseProductName());
MapJoin<IdentityProviderMapperEntity, String, String> configJoin = idp.joinMap("config");
Predicate configNamePredicate = builder.equal(configJoin.key(), key);
return closing(typedQuery.getResultStream().map(this::toModel)); if (dbProductName.equals("Oracle")) {
// Oracle is not able to compare a CLOB with a VARCHAR unless it being converted with TO_CHAR
// But for this all values in the table need to be smaller than 4K, otherwise the cast will fail with
// "ORA-22835: Buffer too small for CLOB to CHAR" (even if it is in another row).
// This leaves DBMS_LOB.COMPARE and DBMS_LOB.INSTR as the options to compare the CLOB with the value.
if (value.endsWith("*")) {
// prefix search - use DBMS_LOB.INSTR
value = value.substring(0, value.length() - 1);
Predicate configValuePredicate = builder.equal(builder.function("DBMS_LOB.INSTR", Integer.class, configJoin.value(), builder.literal(value)), 1);
predicates.add(builder.and(configNamePredicate, configValuePredicate));
} else {
Predicate configValuePredicate = builder.equal(builder.function("DBMS_LOB.COMPARE", Integer.class, configJoin.value(), builder.literal(value)), 0);
predicates.add(builder.and(configNamePredicate, configValuePredicate));
}
} else {
if (value.endsWith("*")) {
value = value.replace("%", "\\%").replace("_", "\\_").replace("*", "%");
predicates.add(builder.and(configNamePredicate, builder.like(configJoin.value(), value)));
} else {
predicates.add(builder.and(configNamePredicate, builder.equal(configJoin.value(), value)));
}
}
}
}
TypedQuery<IdentityProviderMapperEntity> typedQuery = em.createQuery(query.select(idp).where(predicates.toArray(Predicate[]::new)));
return closing(paginateQuery(typedQuery, first, max).getResultStream()).map(this::toModel);
} }
@Override @Override

View file

@ -32,40 +32,14 @@ import java.util.function.Consumer;
* @author <a href="mailto:daniel.fesenmeyer@bosch.io">Daniel Fesenmeyer</a> * @author <a href="mailto:daniel.fesenmeyer@bosch.io">Daniel Fesenmeyer</a>
*/ */
public abstract class AbstractConfigPropertySynchronizer<T extends ProviderEvent> implements ConfigSynchronizer<T> { public abstract class AbstractConfigPropertySynchronizer<T extends ProviderEvent> implements ConfigSynchronizer<T> {
private static final Logger LOG = Logger.getLogger(AbstractConfigPropertySynchronizer.class); private static final Logger LOG = Logger.getLogger(AbstractConfigPropertySynchronizer.class);
protected abstract String getConfigPropertyName(); protected void logEventProcessed(String configPropertyName, String previousValue, String newValue, String realmName,
String mapperName, String idpAlias) {
protected abstract void updateConfigPropertyIfNecessary(T event, String currentPropertyValue,
Consumer<String> propertyUpdater);
@Override
public final void handleEvent(T event, IdentityProviderMapperModel idpMapper) {
Map<String, String> config = idpMapper.getConfig();
if (config == null) {
return;
}
String configPropertyName = getConfigPropertyName();
String configuredValue = config.get(configPropertyName);
if (StringUtil.isBlank(configuredValue)) {
return;
}
Consumer<String> propertyUpdater = value -> config.put(configPropertyName, value);
updateConfigPropertyIfNecessary(event, configuredValue, propertyUpdater);
final String newConfiguredValue = config.get(configPropertyName);
if (!configuredValue.equals(newConfiguredValue)) {
RealmModel realm = extractRealm(event);
LOG.infof( LOG.infof(
"Reference of type '%s' changed from '%s' to '%s' in realm '%s'. Adjusting the reference from mapper '%s' of IDP '%s'.", "Reference of type '%s' changed from '%s' to '%s' in realm '%s'. Adjusting the reference from mapper '%s' of IDP '%s'.",
configPropertyName, configuredValue, newConfiguredValue, realm.getName(), idpMapper.getName(), configPropertyName, previousValue, newValue, realmName, mapperName, idpAlias);
idpMapper.getIdentityProviderAlias());
getKeycloakSession(event).identityProviders().updateMapper(idpMapper);
}
}
}
} }

View file

@ -42,24 +42,12 @@ public final class ConfigSyncEventListener implements ProviderEventListener {
@Override @Override
public void onEvent(ProviderEvent event) { public void onEvent(ProviderEvent event) {
List<IdentityProviderMapperModel> realmMappers = null;
for (ConfigSynchronizer<? extends ProviderEvent> s : SYNCHRONIZERS) { for (ConfigSynchronizer<? extends ProviderEvent> s : SYNCHRONIZERS) {
ConfigSynchronizer<ProviderEvent> configSynchronizer = (ConfigSynchronizer<ProviderEvent>) s; ConfigSynchronizer<ProviderEvent> configSynchronizer = (ConfigSynchronizer<ProviderEvent>) s;
if (eventMatchesSynchronizer(event, configSynchronizer)) { if (eventMatchesSynchronizer(event, configSynchronizer)) {
LOG.debugf("Synchronizer %s matches event: %s", configSynchronizer, event); LOG.debugf("Synchronizer %s matches event: %s", configSynchronizer, event);
configSynchronizer.handleEvent(event);
if (realmMappers == null) {
realmMappers = configSynchronizer.getKeycloakSession(event).identityProviders().getMappersStream()
.collect(Collectors.toList());
}
realmMappers.forEach(idpMapper -> {
LOG.debugf("Apply synchronizer %s to event %s and mapper with name %s", configSynchronizer, event,
idpMapper.getName());
configSynchronizer.handleEvent(event, idpMapper);
});
} else { } else {
LOG.debugf("Synchronizer %s does not match event: %s", configSynchronizer, event); LOG.debugf("Synchronizer %s does not match event: %s", configSynchronizer, event);
} }

View file

@ -30,9 +30,5 @@ import org.keycloak.provider.ProviderEvent;
public interface ConfigSynchronizer<T extends ProviderEvent> { public interface ConfigSynchronizer<T extends ProviderEvent> {
Class<T> getEventClass(); Class<T> getEventClass();
RealmModel extractRealm(T event); void handleEvent(T event);
KeycloakSession getKeycloakSession(T event);
void handleEvent(T event, IdentityProviderMapperModel idpMapper);
} }

View file

@ -23,6 +23,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.keycloak.models.utils.KeycloakModelUtils.GROUP_PATH_SEPARATOR; import static org.keycloak.models.utils.KeycloakModelUtils.GROUP_PATH_SEPARATOR;
@ -46,37 +47,27 @@ public class GroupConfigPropertyByPathSynchronizer extends AbstractConfigPropert
} }
@Override @Override
public RealmModel extractRealm(GroupModel.GroupPathChangeEvent event) { public void handleEvent(GroupModel.GroupPathChangeEvent event) {
return event.getRealm();
}
@Override // first find all mappers that have a group config property that maps exactly to the changed path.
public KeycloakSession getKeycloakSession(GroupModel.GroupPathChangeEvent event) { event.getKeycloakSession().identityProviders().getMappersStream(Map.of(ConfigConstants.GROUP, event.getPreviousPath()), null, null)
return event.getKeycloakSession(); .forEach(idpMapper -> {
} idpMapper.getConfig().put(ConfigConstants.GROUP, event.getNewPath());
super.logEventProcessed(ConfigConstants.GROUP, event.getPreviousPath(), event.getNewPath(), event.getRealm().getName(),
idpMapper.getName(), idpMapper.getIdentityProviderAlias());
event.getKeycloakSession().identityProviders().updateMapper(idpMapper);
});
@Override // then find all mappers that have a group config that maps to a sub-path of the changed path.
public String getConfigPropertyName() { event.getKeycloakSession().identityProviders().getMappersStream(
return ConfigConstants.GROUP; Map.of(ConfigConstants.GROUP, event.getPreviousPath() + GROUP_PATH_SEPARATOR + "*"), null, null)
.forEach(idpMapper -> {
String currentGroupPath = idpMapper.getConfig().get(ConfigConstants.GROUP);
String newGroupPath = event.getNewPath() + currentGroupPath.substring(event.getPreviousPath().length());
idpMapper.getConfig().put(ConfigConstants.GROUP, newGroupPath);
super.logEventProcessed(ConfigConstants.GROUP, currentGroupPath, newGroupPath, event.getRealm().getName(),
idpMapper.getName(), idpMapper.getIdentityProviderAlias());
event.getKeycloakSession().identityProviders().updateMapper(idpMapper);
});
} }
@Override
protected void updateConfigPropertyIfNecessary(GroupModel.GroupPathChangeEvent event,
String currentPropertyValue, Consumer<String> propertyUpdater) {
String configuredGroupPath = KeycloakModelUtils.normalizeGroupPath(currentPropertyValue);
String previousGroupPath = event.getPreviousPath();
if (configuredGroupPath.equals(previousGroupPath)) {
String newPath = event.getNewPath();
propertyUpdater.accept(newPath);
} else if (isSubGroupOf(configuredGroupPath, previousGroupPath)) {
String newPath = event.getNewPath() + configuredGroupPath.substring(previousGroupPath.length());
propertyUpdater.accept(newPath);
}
}
private static boolean isSubGroupOf(String groupPath, String potentialParentGroupPath) {
return groupPath.startsWith(potentialParentGroupPath + GROUP_PATH_SEPARATOR);
}
} }

View file

@ -23,6 +23,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@ -30,8 +31,7 @@ import java.util.function.Consumer;
* *
* @author <a href="mailto:daniel.fesenmeyer@bosch.io">Daniel Fesenmeyer</a> * @author <a href="mailto:daniel.fesenmeyer@bosch.io">Daniel Fesenmeyer</a>
*/ */
public class RoleConfigPropertyByClientIdSynchronizer public class RoleConfigPropertyByClientIdSynchronizer extends AbstractConfigPropertySynchronizer<ClientModel.ClientIdChangeEvent> {
extends AbstractConfigPropertySynchronizer<ClientModel.ClientIdChangeEvent> {
public static final RoleConfigPropertyByClientIdSynchronizer INSTANCE = public static final RoleConfigPropertyByClientIdSynchronizer INSTANCE =
new RoleConfigPropertyByClientIdSynchronizer(); new RoleConfigPropertyByClientIdSynchronizer();
@ -46,36 +46,17 @@ public class RoleConfigPropertyByClientIdSynchronizer
} }
@Override @Override
public RealmModel extractRealm(ClientModel.ClientIdChangeEvent event) { public void handleEvent(ClientModel.ClientIdChangeEvent event) {
return event.getUpdatedClient().getRealm(); // find all mappers that have a role config property that maps to a role associated with the changed client.
event.getKeycloakSession().identityProviders().getMappersStream(Map.of(ConfigConstants.ROLE, event.getPreviousClientId() + ".*"), null, null)
.forEach(idpMapper -> {
String currentRoleValue = idpMapper.getConfig().get(ConfigConstants.ROLE);
String configuredRoleName = KeycloakModelUtils.parseRole(currentRoleValue)[1];
String newRoleValue = KeycloakModelUtils.buildRoleQualifier(event.getNewClientId(), configuredRoleName);
idpMapper.getConfig().put(ConfigConstants.ROLE, newRoleValue);
super.logEventProcessed(ConfigConstants.ROLE, currentRoleValue, newRoleValue, event.getUpdatedClient().getRealm().getName(),
idpMapper.getName(), idpMapper.getIdentityProviderAlias());
event.getKeycloakSession().identityProviders().updateMapper(idpMapper);
});
} }
@Override
public KeycloakSession getKeycloakSession(ClientModel.ClientIdChangeEvent event) {
return event.getKeycloakSession();
}
@Override
public String getConfigPropertyName() {
return ConfigConstants.ROLE;
}
@Override
protected void updateConfigPropertyIfNecessary(ClientModel.ClientIdChangeEvent event, String currentPropertyValue,
Consumer<String> propertyUpdater) {
String[] parsedConfiguredRoleQualifier = KeycloakModelUtils.parseRole(currentPropertyValue);
String configuredClientId = parsedConfiguredRoleQualifier[0];
if (configuredClientId == null) {
// a realm role is configured for the mapper, event is not relevant
return;
}
String configuredRoleName = parsedConfiguredRoleQualifier[1];
if (configuredClientId.equals(event.getPreviousClientId())) {
String newRoleQualifier = KeycloakModelUtils.buildRoleQualifier(event.getNewClientId(), configuredRoleName);
propertyUpdater.accept(newRoleQualifier);
}
}
} }

View file

@ -23,6 +23,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@ -46,30 +47,17 @@ public class RoleConfigPropertyByRoleNameSynchronizer
} }
@Override @Override
public RealmModel extractRealm(RoleModel.RoleNameChangeEvent event) { public void handleEvent(RoleModel.RoleNameChangeEvent event) {
return event.getRealm(); // first find all mappers that have a role config property that maps exactly to the changed path.
} String currentRoleValue = KeycloakModelUtils.buildRoleQualifier(event.getClientId(), event.getPreviousName());
event.getKeycloakSession().identityProviders().getMappersStream(Map.of(ConfigConstants.ROLE, currentRoleValue), null, null)
.forEach(idpMapper -> {
String newRoleValue = KeycloakModelUtils.buildRoleQualifier(event.getClientId(), event.getNewName());
idpMapper.getConfig().put(ConfigConstants.ROLE, newRoleValue);
super.logEventProcessed(ConfigConstants.ROLE, currentRoleValue, newRoleValue, event.getRealm().getName(),
idpMapper.getName(), idpMapper.getIdentityProviderAlias());
event.getKeycloakSession().identityProviders().updateMapper(idpMapper);
});
@Override
public KeycloakSession getKeycloakSession(RoleModel.RoleNameChangeEvent event) {
return event.getKeycloakSession();
} }
@Override
public String getConfigPropertyName() {
return ConfigConstants.ROLE;
}
@Override
protected void updateConfigPropertyIfNecessary(RoleModel.RoleNameChangeEvent event, String currentPropertyValue,
Consumer<String> propertyUpdater) {
String previousRoleQualifier =
KeycloakModelUtils.buildRoleQualifier(event.getClientId(), event.getPreviousName());
if (previousRoleQualifier.equals(currentPropertyValue)) {
String newRoleQualifier = KeycloakModelUtils.buildRoleQualifier(event.getClientId(), event.getNewName());
propertyUpdater.accept(newRoleQualifier);
}
}
} }

View file

@ -102,14 +102,23 @@ public interface IdentityProviderStorageProvider extends Provider {
} }
/** /**
* Returns all identity providers in the realm filtered according to the specified parameters. * Returns all identity providers in the realm filtered according to the specified search options. The options include:
* <ul>
* <li>Regular fields found in {@link IdentityProviderModel}, such as {@code ALIAS}, {@code ENABLED}, {@code HIDE_ON_LOGIN}, etc;</li>
* <li>Special search keys also present in {@link IdentityProviderModel}. Those include {@code SEARCH}, used to perform
* exact, prefix, and infix searches by alias, and {@code ALIAS_NOT_IN}, used to perform searches for identity providers
* whose alias doesn't match any of the specified aliases (separated by comma);</li>
* <li>Any attribute found in the identity provider's config. If the option key doesn't match any of the previous
* cases, the implementations must search the providers whose config contains a pair that matches the specified search
* option</li>
* </ul>
* *
* @param attrs a {@code Map} containig identity provider config attributes that must be matched. * @param options a {@link Map} containing identity provider search options that must be matched.
* @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}. * @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}.
* @param max the maximum number of results to be returned. Ignored if negative or {@code null}. * @param max the maximum number of results to be returned. Ignored if negative or {@code null}.
* @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria. * @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria.
*/ */
Stream<IdentityProviderModel> getAllStream(Map<String, String> attrs, Integer first, Integer max); Stream<IdentityProviderModel> getAllStream(Map<String, String> options, Integer first, Integer max);
/** /**
* Returns all identity providers associated with the organization with the provided id. * Returns all identity providers associated with the organization with the provided id.
@ -295,7 +304,19 @@ public interface IdentityProviderStorageProvider extends Provider {
* Returns all identity provider mappers as a stream. * Returns all identity provider mappers as a stream.
* @return Stream of {@link IdentityProviderMapperModel}. Never returns {@code null}. * @return Stream of {@link IdentityProviderMapperModel}. Never returns {@code null}.
*/ */
Stream<IdentityProviderMapperModel> getMappersStream(); default Stream<IdentityProviderMapperModel> getMappersStream() {
return this.getMappersStream(Map.of(), null, null);
}
/**
* Returns all identity provider mappers in the realm filtered according to the specified search options.
*
* @param options a {@link Map} containing identity provider search options that must be matched.
* @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}.
* @param max the maximum number of results to be returned. Ignored if negative or {@code null}.
* @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria.
*/
Stream<IdentityProviderMapperModel> getMappersStream(Map<String, String> options, Integer first, Integer max);
/** /**
* Returns identity provider mappers by the provided alias as a stream. * Returns identity provider mappers by the provided alias as a stream.

View file

@ -15,6 +15,8 @@ SSOTest
SamlClientTest SamlClientTest
TransactionsTest TransactionsTest
UserProfileTest UserProfileTest
OidcAdvancedClaimToGroupMapperTest
OidcAdvancedClaimToRoleMapperTest
org.keycloak.testsuite.admin.** org.keycloak.testsuite.admin.**
org.keycloak.testsuite.authz.**ManagementTest org.keycloak.testsuite.authz.**ManagementTest
org.keycloak.testsuite.organization.admin.** org.keycloak.testsuite.organization.admin.**