Better caching for federated users

Closes #35637

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc
2024-12-08 12:41:13 +01:00
committed by Pedro Igor
parent 1a1cdf4120
commit bac5ec8858
4 changed files with 147 additions and 13 deletions

View File

@@ -169,7 +169,7 @@ public abstract class CacheManager {
return;
}
if (rev.equals(object.getRevision())) {
cache.putForExternalRead(id, object);
put(id, object, lifespan);
return;
}
if (rev > object.getRevision()) { // revision is ahead, don't cache
@@ -178,8 +178,7 @@ public abstract class CacheManager {
}
// revisions cache has a lower value than the object.revision, so update revision and add it to cache
revisions.put(id, object.getRevision());
if (lifespan < 0) cache.putForExternalRead(id, object);
else cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
put(id, object, lifespan);
} finally {
endRevisionBatch();
}
@@ -198,6 +197,14 @@ public abstract class CacheManager {
}
}
private void put(String id, Revisioned object, long lifespan) {
if (lifespan < 0) {
cache.putForExternalRead(id, object);
} else {
cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
}
}
private Iterator<Map.Entry<String, Revisioned>> getEntryIterator(Predicate<Map.Entry<String, Revisioned>> predicate) {
return cache
.entrySet()

View File

@@ -77,6 +77,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -226,7 +227,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
}
adapter = cacheUser(realm, delegate, loaded);
} else {
adapter = validateCache(realm, cached);
adapter = validateCache(realm, cached, () -> getDelegate().getUserById(realm, id));
}
managedUsers.put(id, adapter);
return adapter;
@@ -307,11 +308,11 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
if (cached == null) {
return cacheUser(realm, delegate, loaded);
} else {
return validateCache(realm, cached);
return validateCache(realm, cached, () -> getDelegate().getUserById(realm, userId));
}
}
protected UserModel validateCache(RealmModel realm, CachedUser cached) {
protected UserModel validateCache(RealmModel realm, CachedUser cached, Supplier<UserModel> supplier) {
if (!realm.getId().equals(cached.getRealm())) {
return null;
}
@@ -330,9 +331,10 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
// its also hard to test stuff
if (model.shouldInvalidate(cached)) {
registerUserInvalidation(cached);
return getDelegate().getUserById(realm, cached.getId());
return supplier.get();
}
}
return new UserAdapter(cached, this, session, realm);
}
@@ -589,29 +591,47 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
return getDelegate().getUsersCount(realm, params, groupIds);
}
private UserModel returnFromCacheIfPresent(RealmModel realm, UserModel delegate) {
if (delegate == null || delegate instanceof CachedUserModel || isRegisteredForInvalidation(realm, delegate.getId())) {
return delegate;
}
if (managedUsers.containsKey(delegate.getId())) {
return managedUsers.get(delegate.getId());
}
CachedUser cached = cache.get(delegate.getId(), CachedUser.class);
if (cached == null) {
return delegate;
}
UserModel cachedUserModel = validateCache(realm, cached, () -> delegate);
return cachedUserModel != null? cachedUserModel : delegate;
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, String search) {
return getDelegate().searchForUserStream(realm, search);
return getDelegate().searchForUserStream(realm, search).map(u -> returnFromCacheIfPresent(realm, u));
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
return getDelegate().searchForUserStream(realm, search, firstResult, maxResults);
return getDelegate().searchForUserStream(realm, search, firstResult, maxResults).map(u -> returnFromCacheIfPresent(realm, u));
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes) {
return getDelegate().searchForUserStream(realm, attributes);
return getDelegate().searchForUserStream(realm, attributes).map(u -> returnFromCacheIfPresent(realm, u));
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
return getDelegate().searchForUserStream(realm, attributes, firstResult, maxResults);
return getDelegate().searchForUserStream(realm, attributes, firstResult, maxResults).map(u -> returnFromCacheIfPresent(realm, u));
}
@Override
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
return getDelegate().searchForUserByUserAttributeStream(realm, attrName, attrValue);
return getDelegate().searchForUserByUserAttributeStream(realm, attrName, attrValue).map(u -> returnFromCacheIfPresent(realm, u));
}
@Override

View File

@@ -51,7 +51,6 @@ import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@@ -115,6 +114,9 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
protected UserModel importValidation(RealmModel realm, UserModel user) {
if (isReadOnlyOrganizationMember(user)) {
if (user instanceof CachedUserModel cachedUserModel) {
cachedUserModel.invalidate();
}
return new ReadOnlyUserModelDelegate(user, false);
}
@@ -129,9 +131,17 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
if (!model.isEnabled()) {
if (user instanceof CachedUserModel cachedUserModel) {
cachedUserModel.invalidate();
}
return new ReadOnlyUserModelDelegate(user, false);
}
if (user instanceof CachedUserModel) {
// if the user is cached do not validate import for the cached configured time
return user;
}
ImportedUserValidation importedUserValidation = getStorageProviderInstance(model, ImportedUserValidation.class, true);
if (importedUserValidation == null) return user;

View File

@@ -1385,6 +1385,103 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
setTimeOffset(0);
}
@Test
public void testAlwaysReadValueFromLdapCached() throws Exception {
try {
// import user from the ldap johnkeycloak and cache it reading it by id
List<UserRepresentation> users = testRealm().users().search("johnkeycloak", true);
Assert.assertEquals(1, users.size());
UserRepresentation john = users.iterator().next();
Assert.assertEquals("Doe", john.getLastName());
john = testRealm().users().get(john.getId()).toRepresentation();
Assert.assertEquals("Doe", john.getLastName());
// modify the sn of the user directly in ldap
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
LDAPObject johnLdapObject = ctx.getLdapProvider().loadLDAPUserByUsername(ctx.getRealm(), "johnkeycloak");
johnLdapObject.setSingleAttribute(LDAPConstants.SN, "sn-modified");
ctx.getLdapProvider().getLdapIdentityStore().update(johnLdapObject);
});
// it's cached so it should be still the initial one
users = testRealm().users().search("johnkeycloak", true);
Assert.assertEquals(1, users.size());
john = users.iterator().next();
Assert.assertEquals("Doe", john.getLastName());
john = testRealm().users().get(john.getId()).toRepresentation();
Assert.assertEquals("Doe", john.getLastName());
// expire the cache which is 10 minutes
setTimeOffset(610);
// new sn should be present
users = testRealm().users().search("johnkeycloak", true);
Assert.assertEquals(1, users.size());
john = users.iterator().next();
Assert.assertEquals("sn-modified", john.getLastName());
john = testRealm().users().get(john.getId()).toRepresentation();
Assert.assertEquals("sn-modified", john.getLastName());
} finally {
// revert
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
LDAPObject johnLdapObject = ctx.getLdapProvider().loadLDAPUserByUsername(ctx.getRealm(), "johnkeycloak");
johnLdapObject.setSingleAttribute(LDAPConstants.SN, "Doe");
ctx.getLdapProvider().getLdapIdentityStore().update(johnLdapObject);
});
}
}
@Test
public void testAlwaysReadValueFromLdapNoCache() throws Exception {
// set to NO_CACHE
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
ctx.getLdapModel().setCachePolicy(UserStorageProviderModel.CachePolicy.NO_CACHE);
appRealm.updateComponent(ctx.getLdapModel());
});
try {
// import user from the ldap johnkeycloak
List<UserRepresentation> users = testRealm().users().search("johnkeycloak", true);
Assert.assertEquals(1, users.size());
UserRepresentation john = users.iterator().next();
Assert.assertEquals("Doe", john.getLastName());
john = testRealm().users().get(john.getId()).toRepresentation();
Assert.assertEquals("Doe", john.getLastName());
// modify the sn of the user directly in ldap
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
LDAPObject johnLdapObject = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "johnkeycloak");
johnLdapObject.setSingleAttribute(LDAPConstants.SN, "sn-modified");
ctx.getLdapProvider().getLdapIdentityStore().update(johnLdapObject);
});
// no cache, so it should be validated and new data received
users = testRealm().users().search("johnkeycloak", true);
Assert.assertEquals(1, users.size());
john = users.iterator().next();
Assert.assertEquals("sn-modified", john.getLastName());
john = testRealm().users().get(john.getId()).toRepresentation();
Assert.assertEquals("sn-modified", john.getLastName());
} finally {
// revert cache to default max-liespan setting
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
LDAPObject johnLdapObject = ctx.getLdapProvider().loadLDAPUserByUsername(ctx.getRealm(), "johnkeycloak");
johnLdapObject.setSingleAttribute(LDAPConstants.SN, "Doe");
ctx.getLdapProvider().getLdapIdentityStore().update(johnLdapObject);
ctx.getLdapModel().setCachePolicy(UserStorageProviderModel.CachePolicy.MAX_LIFESPAN);
ctx.getLdapModel().setMaxLifespan(600000);
ctx.getRealm().updateComponent(ctx.getLdapModel());
});
}
}
@Test
public void testEmailVerifiedFromImport(){