+
Skip to content

Invalidate user cache entries when email or username are different from storage #40256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -667,15 +667,8 @@ protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm

private void doImportUser(final RealmModel realm, final UserModel user, final LDAPObject ldapUser) {
user.setEnabled(true);
realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
.sorted(ldapMappersComparator.sortDesc())
.forEachOrdered(mapperModel -> {
if (logger.isTraceEnabled()) {
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
}
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
ldapMapper.onImportUserFromLDAP(ldapUser, user, realm, true);
});

importUserAttributes(realm, user, ldapUser);

String userDN = ldapUser.getDn().toString();
if (model.isImportEnabled()) user.setFederationLink(model.getId());
Expand Down Expand Up @@ -784,6 +777,7 @@ public UserModel getUserByEmail(RealmModel realm, String email) {
LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
// If email attribute mapper is set to "Always Read Value From LDAP" the user may be in Keycloak DB with an old email address
if (ldapUser.getUuid().equals(user.getFirstAttribute(LDAPConstants.LDAP_ID))) {
importUserAttributes(realm, user, ldapUser);
return proxy(realm, user, ldapUser, false);
}
throw new ModelDuplicateException("User with username '" + ldapUsername + "' already exists in Keycloak. It conflicts with LDAP user with email '" + email + "'");
Expand Down Expand Up @@ -1240,4 +1234,16 @@ private long getPasswordChangedTime(LDAPObject ldapObject) {
}
return LDAPUtils.generalizedTimeToDate(value).getTime();
}

private void importUserAttributes(RealmModel realm, UserModel user, LDAPObject ldapUser) {
realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
.sorted(ldapMappersComparator.sortDesc())
.forEachOrdered(mapperModel -> {
if (logger.isTraceEnabled()) {
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
}
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
ldapMapper.onImportUserFromLDAP(ldapUser, user, realm, true);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,22 @@ public void setEmailVerified(boolean verified) {
super.setEmailVerified(verified);
}

@Override
public String getUsername() {
if (UserModel.USERNAME.equals(userModelAttrName)) {
return ldapUser.getAttributeAsString(ldapAttrName);
}
return super.getUsername();
}

@Override
public String getEmail() {
if (UserModel.EMAIL.equals(userModelAttrName)) {
return ldapUser.getAttributeAsString(ldapAttrName);
}
return super.getEmail();
}

protected boolean setLDAPAttribute(String modelAttrName, Object value) {
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
if (UserAttributeLDAPStorageMapper.logger.isTraceEnabled()) {
Expand Down Expand Up @@ -506,11 +522,18 @@ protected void setPropertyOnUserModel(Property<Object> userModelProperty, UserMo
userModelProperty.setValue(user, null);
} else {
Class<Object> clazz = userModelProperty.getJavaClass();
Object currentValue = userModelProperty.getValue(user);

if (String.class.equals(clazz)) {
if (ldapAttrValue.equals(currentValue)) {
return;
}
userModelProperty.setValue(user, ldapAttrValue);
} else if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) {
Boolean boolVal = Boolean.valueOf(ldapAttrValue);
if (boolVal.equals(currentValue)) {
return;
}
userModelProperty.setValue(user, boolVal);
} else {
logger.warnf("Don't know how to set the property '%s' on user '%s' . Value of LDAP attribute is '%s' ", userModelProperty.getName(), user.getUsername(), ldapAttrValue.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public void addInvalidations(Predicate<Map.Entry<String, Revisioned>> predicate,
private void put(String id, Revisioned object, long lifespan) {
if (lifespan < 0) {
cache.putForExternalRead(id, object);
} else {
} else if (lifespan > 0) {
cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1261,8 +1261,6 @@ protected ClientModel validateCache(RealmModel realm, CachedClient cached) {
}
ClientStorageProviderModel model = new ClientStorageProviderModel(component);

// although we do set a timeout, Infinispan has no guarantees when the user will be evicted
// its also hard to test stuff
if (model.shouldInvalidate(cached)) {
registerClientInvalidation(cached.getId(), cached.getClientId(), realm.getId());
return getClientDelegate().getClientById(realm, cached.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.keycloak.models.cache.infinispan;

import static java.util.Optional.ofNullable;
import static org.keycloak.organization.utils.Organizations.isReadOnlyOrganizationMember;

import org.jboss.logging.Logger;
Expand Down Expand Up @@ -254,9 +255,12 @@ static String getFederatedIdentityLinksCacheKey(String userId) {
}

@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
public UserModel getUserByUsername(RealmModel realm, String rawUsername) {
if (rawUsername == null) {
return null;
}
String username = rawUsername.toLowerCase();
logger.tracev("getUserByUsername: {0}", username);
username = username.toLowerCase();
if (realmInvalidations.contains(realm.getId())) {
logger.tracev("realmInvalidations");
return getDelegate().getUserByUsername(realm, username);
Expand All @@ -268,7 +272,6 @@ public UserModel getUserByUsername(RealmModel realm, String username) {
}
UserListQuery query = cache.get(cacheKey, UserListQuery.class);

String userId = null;
if (query == null) {
logger.tracev("query null");
Long loaded = cache.getCurrentRevision(cacheKey);
Expand All @@ -277,7 +280,7 @@ public UserModel getUserByUsername(RealmModel realm, String username) {
logger.tracev("model from delegate null");
return null;
}
userId = model.getId();
String userId = model.getId();
if (invalidations.contains(userId)) return model;
if (managedUsers.containsKey(userId)) {
logger.tracev("return managed user");
Expand All @@ -287,20 +290,59 @@ public UserModel getUserByUsername(RealmModel realm, String username) {
UserModel adapter = getUserAdapter(realm, userId, loaded, model);
if (adapter instanceof UserAdapter) { // this was cached, so we can cache query too
query = new UserListQuery(loaded, cacheKey, realm, model.getId());
cache.addRevisioned(query, startupRevision);
cache.addRevisioned(query, startupRevision, getLifespan(realm, adapter));
}
managedUsers.put(userId, adapter);
return adapter;
} else {
userId = query.getUsers().iterator().next();
if (invalidations.contains(userId)) {
logger.tracev("invalidated cache return delegate");
return getDelegate().getUserByUsername(realm, username);
}

String userId = query.getUsers().iterator().next();
if (invalidations.contains(userId)) {
logger.tracev("invalidated cache return delegate");
return getDelegate().getUserByUsername(realm, username);

}
logger.trace("return getUserById");
return ofNullable(getUserById(realm, userId))
// Validate for cases where the cached elements are not in sync.
// This might happen to changes in a federated store where caching is enabled and different items expire at different times,
// for example when they are evicted due to the limited size of the cache
.filter((u) -> username.equalsIgnoreCase(u.getUsername()))
.orElseGet(() -> {
registerInvalidation(cacheKey);
return getDelegate().getUserByUsername(realm, username);
});
}

private long getLifespan(RealmModel realm, UserModel user) {
if (!user.isFederated()) {
return -1; // cache infinite
}

String providerId = user.getFederationLink();

if (providerId == null) {
providerId = StorageId.providerId(user.getId());
}

ComponentModel component = realm.getComponent(providerId);
UserStorageProviderModel model = new UserStorageProviderModel(component);

if (model.isEnabled()) {
UserStorageProviderModel.CachePolicy policy = model.getCachePolicy();

if (policy == null) {
// no policy set, cache entries by default
return -1;
}

if (!UserStorageProviderModel.CachePolicy.NO_CACHE.equals(policy)) {
long lifespan = model.getLifespan();
return lifespan > 0 ? lifespan : -1;
}
logger.trace("return getUserById");
return getUserById(realm, userId);
}

return 0; // do not cache
}

protected UserModel getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel delegate) {
Expand All @@ -327,8 +369,6 @@ protected UserModel validateCache(RealmModel realm, CachedUser cached, Supplier<
}
CacheableStorageProviderModel model = new CacheableStorageProviderModel(component);

// although we do set a timeout, Infinispan has no guarantees when the user will be evicted
// its also hard to test stuff
if (model.shouldInvalidate(cached)) {
registerUserInvalidation(cached);
return supplier.get();
Expand All @@ -354,21 +394,17 @@ protected UserModel cacheUser(RealmModel realm, UserModel delegate, Long revisio
if (!model.isEnabled()) {
return new ReadOnlyUserModelDelegate(delegate, false);
}
UserStorageProviderModel.CachePolicy policy = model.getCachePolicy();
if (policy != null && policy == UserStorageProviderModel.CachePolicy.NO_CACHE) {

long lifespan = getLifespan(realm, delegate);
if (lifespan == 0) {
return delegate;
}

cached = new CachedUser(revision, realm, delegate, notBefore);
adapter = new UserAdapter(cached, this, session, realm);
onCache(realm, adapter, delegate);

long lifespan = model.getLifespan();
if (lifespan > 0) {
cache.addRevisioned(cached, startupRevision, lifespan);
} else {
cache.addRevisioned(cached, startupRevision);
}
cache.addRevisioned(cached, startupRevision, lifespan);
} else {
cached = new CachedUser(revision, realm, delegate, notBefore);
adapter = new UserAdapter(cached, this, session, realm);
Expand All @@ -384,9 +420,9 @@ private void onCache(RealmModel realm, UserAdapter adapter, UserModel delegate)
}

@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
if (email == null) return null;
email = email.toLowerCase();
public UserModel getUserByEmail(RealmModel realm, String rawEmail) {
if (rawEmail == null) return null;
String email = rawEmail.toLowerCase();
if (realmInvalidations.contains(realm.getId())) {
return getDelegate().getUserByEmail(realm, email);
}
Expand All @@ -396,30 +432,37 @@ public UserModel getUserByEmail(RealmModel realm, String email) {
}
UserListQuery query = cache.get(cacheKey, UserListQuery.class);

String userId = null;
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
UserModel model = getDelegate().getUserByEmail(realm, email);
if (model == null) return null;
userId = model.getId();
String userId = model.getId();
if (invalidations.contains(userId)) return model;
if (managedUsers.containsKey(userId)) return managedUsers.get(userId);

UserModel adapter = getUserAdapter(realm, userId, loaded, model);
if (adapter instanceof UserAdapter) {
query = new UserListQuery(loaded, cacheKey, realm, model.getId());
cache.addRevisioned(query, startupRevision);
cache.addRevisioned(query, startupRevision, getLifespan(realm, adapter));
}
managedUsers.put(userId, adapter);
return adapter;
} else {
userId = query.getUsers().iterator().next();
if (invalidations.contains(userId)) {
return getDelegate().getUserByEmail(realm, email);
}

String userId = query.getUsers().iterator().next();
if (invalidations.contains(userId)) {
return getDelegate().getUserByEmail(realm, email);

}
return getUserById(realm, userId);
}
return ofNullable(getUserById(realm, userId))
// Validate for cases where the cached elements are not in sync.
// This might happen to changes in a federated store where caching is enabled and different items expire at different times,
// for example when they are evicted due to the limited size of the cache
.filter((u) -> email.equalsIgnoreCase(u.getEmail()))
.orElseGet(() -> {
registerInvalidation(cacheKey);
return getDelegate().getUserByEmail(realm, email);
});
}

@Override
Expand Down Expand Up @@ -453,7 +496,7 @@ public UserModel getUserByFederatedIdentity(RealmModel realm, FederatedIdentityM
UserModel adapter = getUserAdapter(realm, userId, loaded, model);
if (adapter instanceof UserAdapter) {
query = new UserListQuery(loaded, cacheKey, realm, model.getId());
cache.addRevisioned(query, startupRevision);
cache.addRevisioned(query, startupRevision, getLifespan(realm, adapter));
}

managedUsers.put(userId, adapter);
Expand Down Expand Up @@ -540,7 +583,7 @@ public UserModel findServiceAccount(ClientModel client) {
UserModel adapter = getUserAdapter(realm, userId, loaded, model);
if (adapter instanceof UserAdapter) { // this was cached, so we can cache query too
query = new UserListQuery(loaded, cacheKey, realm, model.getId());
cache.addRevisioned(query, startupRevision);
cache.addRevisioned(query, startupRevision, getLifespan(realm, adapter));
}
managedUsers.put(userId, adapter);
return adapter;
Expand Down Expand Up @@ -650,7 +693,7 @@ public Stream<FederatedIdentityModel> getFederatedIdentitiesStream(RealmModel re
Set<FederatedIdentityModel> federatedIdentities = getDelegate().getFederatedIdentitiesStream(realm, user)
.collect(Collectors.toSet());
cachedLinks = new CachedFederatedIdentityLinks(loaded, cacheKey, realm, federatedIdentities);
cache.addRevisioned(cachedLinks, startupRevision);
cache.addRevisioned(cachedLinks, startupRevision); // this is Keycloak's internal store, cache indefinitely
return federatedIdentities.stream();
} else {
return cachedLinks.getFederatedIdentities().stream();
Expand Down Expand Up @@ -722,7 +765,7 @@ public UserConsentModel getConsentByClient(RealmModel realm, String userId, Stri

Long loaded = cache.getCurrentRevision(cacheKey);
cached = new CachedUserConsents(loaded, cacheKey, realm, consents, false);
cache.addRevisioned(cached, startupRevision);
cache.addRevisioned(cached, startupRevision); // this is from Keycloak's internal store, cache indefinitely
}

Map<String, CachedUserConsent> consents = cached.getConsents();
Expand Down Expand Up @@ -763,7 +806,7 @@ public Stream<UserConsentModel> getConsentsStream(RealmModel realm, String userI
Long loaded = cache.getCurrentRevision(cacheKey);
List<UserConsentModel> consents = getDelegate().getConsentsStream(realm, userId).collect(Collectors.toList());
cached = new CachedUserConsents(loaded, cacheKey, realm, consents.stream().map(CachedUserConsent::new).collect(Collectors.toList()));
cache.addRevisioned(cached, startupRevision);
cache.addRevisioned(cached, startupRevision); // this is from Keycloak's internal store, cache indefinitely
return consents.stream();
} else {
return cached.getConsents().values().stream().map(cachedConsent -> toConsentModel(realm, cachedConsent))
Expand Down
Loading
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载