mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-19 05:20:21 -06:00
Better caching for federated users
Closes #35637 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(){
|
||||
|
||||
|
||||
Reference in New Issue
Block a user