Trigger clearing the user cache when the duplicate email allowed flag changes

Closes #31045

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Alexander Schwartz 2024-07-24 18:46:52 +02:00 committed by Michal Hajas
parent 0b5f42f95d
commit 6d404b86c9
3 changed files with 53 additions and 25 deletions

View file

@ -29,12 +29,11 @@ import org.keycloak.models.cache.UserCache;
import org.keycloak.models.cache.UserCacheProviderFactory; import org.keycloak.models.cache.UserCacheProviderFactory;
import org.keycloak.models.cache.infinispan.entities.Revisioned; import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.provider.InvalidationHandler;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class InfinispanUserCacheProviderFactory implements UserCacheProviderFactory, InvalidationHandler { public class InfinispanUserCacheProviderFactory implements UserCacheProviderFactory {
private static final Logger log = Logger.getLogger(InfinispanUserCacheProviderFactory.class); private static final Logger log = Logger.getLogger(InfinispanUserCacheProviderFactory.class);
public static final String USER_CLEAR_CACHE_EVENTS = "USER_CLEAR_CACHE_EVENTS"; public static final String USER_CLEAR_CACHE_EVENTS = "USER_CLEAR_CACHE_EVENTS";
@ -79,15 +78,6 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
} }
} }
@Override
public void invalidate(KeycloakSession session, InvalidableObjectType type, Object... params) {
if (type == ObjectType.REALM || type == ObjectType.USER) {
if (this.userCache != null) {
this.userCache.clear();
}
}
}
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
} }

View file

@ -18,9 +18,32 @@
package org.keycloak.models.cache.infinispan; package org.keycloak.models.cache.infinispan;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.enums.SslRequired; import org.keycloak.common.enums.SslRequired;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.*; import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.CibaConfig;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuth2DeviceConfig;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.ParConfig;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionConfigModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.cache.CachedRealmModel; import org.keycloak.models.cache.CachedRealmModel;
import org.keycloak.models.cache.UserCache; import org.keycloak.models.cache.UserCache;
import org.keycloak.models.cache.infinispan.entities.CachedRealm; import org.keycloak.models.cache.infinispan.entities.CachedRealm;
@ -28,7 +51,12 @@ import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageUtil; import org.keycloak.storage.UserStorageUtil;
import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.client.ClientStorageProvider;
import java.util.*; import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -362,6 +390,22 @@ public class RealmAdapter implements CachedRealmModel {
@Override @Override
public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) { public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
getDelegateForUpdate(); getDelegateForUpdate();
if (updated.isDuplicateEmailsAllowed() != duplicateEmailsAllowed) {
// If the flag changed, we need to clear all entries from the user cache as there are entries with the key of the email address which need to be re-evaluated.
// Still, this must only happen after all changes have been written to the database, therefore we enlist this to run after the completion of the transaction.
session.getTransactionManager().enlistAfterCompletion(new AbstractKeycloakTransaction() {
@Override
protected void commitImpl() {
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, ClearCacheEvent.getInstance(), false, ClusterProvider.DCNotify.ALL_DCS);
}
@Override
protected void rollbackImpl() {
}
});
}
updated.setDuplicateEmailsAllowed(duplicateEmailsAllowed); updated.setDuplicateEmailsAllowed(duplicateEmailsAllowed);
} }
@ -1074,7 +1118,7 @@ public class RealmAdapter implements CachedRealmModel {
public Stream<RoleModel> getRolesStream() { public Stream<RoleModel> getRolesStream() {
return cacheSession.getRealmRolesStream(this); return cacheSession.getRealmRolesStream(this);
} }
@Override @Override
public Stream<RoleModel> getRolesStream(Integer first, Integer max) { public Stream<RoleModel> getRolesStream(Integer first, Integer max) {
return cacheSession.getRealmRolesStream(this, first, max); return cacheSession.getRealmRolesStream(this, first, max);
@ -1084,7 +1128,7 @@ public class RealmAdapter implements CachedRealmModel {
public Stream<RoleModel> searchForRolesStream(String search, Integer first, Integer max) { public Stream<RoleModel> searchForRolesStream(String search, Integer first, Integer max) {
return cacheSession.searchForRolesStream(this, search, first, max); return cacheSession.searchForRolesStream(this, search, first, max);
} }
@Override @Override
public RoleModel addRole(String name) { public RoleModel addRole(String name) {
return cacheSession.addRealmRole(this, name); return cacheSession.addRealmRole(this, name);
@ -1601,10 +1645,10 @@ public class RealmAdapter implements CachedRealmModel {
public void executeEvictions(ComponentModel model) { public void executeEvictions(ComponentModel model) {
if (model == null) return; if (model == null) return;
// if user cache is disabled this is null // if user cache is disabled this is null
UserCache userCache = UserStorageUtil.userCache(session); UserCache userCache = UserStorageUtil.userCache(session);
if (userCache != null) { if (userCache != null) {
// If not realm component, check to see if it is a user storage provider child component (i.e. LDAP mapper) // If not realm component, check to see if it is a user storage provider child component (i.e. LDAP mapper)
if (model.getParentId() != null && !model.getParentId().equals(getId())) { if (model.getParentId() != null && !model.getParentId().equals(getId())) {
ComponentModel parent = getComponent(model.getParentId()); ComponentModel parent = getComponent(model.getParentId());
@ -1613,13 +1657,13 @@ public class RealmAdapter implements CachedRealmModel {
} }
return; return;
} }
// invalidate entire user cache if we're dealing with user storage SPI // invalidate entire user cache if we're dealing with user storage SPI
if (UserStorageProvider.class.getName().equals(model.getProviderType())) { if (UserStorageProvider.class.getName().equals(model.getProviderType())) {
userCache.evict(this); userCache.evict(this);
} }
} }
// invalidate entire realm if we're dealing with client storage SPI // invalidate entire realm if we're dealing with client storage SPI
// entire realm because of client roles, client lists, and clients // entire realm because of client roles, client lists, and clients
if (ClientStorageProvider.class.getName().equals(model.getProviderType())) { if (ClientStorageProvider.class.getName().equals(model.getProviderType())) {

View file

@ -95,7 +95,6 @@ import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.partialimport.ErrorResponseException; import org.keycloak.partialimport.ErrorResponseException;
import org.keycloak.partialimport.PartialImportResult; import org.keycloak.partialimport.PartialImportResult;
import org.keycloak.partialimport.PartialImportResults; import org.keycloak.partialimport.PartialImportResults;
import org.keycloak.provider.InvalidationHandler;
import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
@ -446,7 +445,6 @@ public class RealmAdminResource {
} }
} }
boolean wasDuplicateEmailsAllowed = realm.isDuplicateEmailsAllowed();
RepresentationToModel.updateRealm(rep, realm, session); RepresentationToModel.updateRealm(rep, realm, session);
// Refresh periodic sync tasks for configured federationProviders // Refresh periodic sync tasks for configured federationProviders
@ -457,10 +455,6 @@ public class RealmAdminResource {
adminEvent.operation(OperationType.UPDATE).representation(rep).success(); adminEvent.operation(OperationType.UPDATE).representation(rep).success();
if (rep.isDuplicateEmailsAllowed() != null && rep.isDuplicateEmailsAllowed() != wasDuplicateEmailsAllowed) {
session.invalidate(InvalidationHandler.ObjectType.REALM, realm.getId());
}
return Response.noContent().build(); return Response.noContent().build();
} catch (ModelDuplicateException e) { } catch (ModelDuplicateException e) {
throw ErrorResponse.exists("Realm with same name exists"); throw ErrorResponse.exists("Realm with same name exists");