keycloak-uncached
Changes
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java 4(+3 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java 169(+144 -25)
model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java 31(+31 -0)
Details
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
index f077927..ad1ba26 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
@@ -13,6 +13,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/**
@@ -126,6 +127,10 @@ public abstract class CacheManager {
}
public void addRevisioned(Revisioned object, long startupRevision) {
+ addRevisioned(object, startupRevision, -1);
+ }
+
+ public void addRevisioned(Revisioned object, long startupRevision, long lifespan) {
//startRevisionBatch();
String id = object.getId();
try {
@@ -164,7 +169,8 @@ public abstract class CacheManager {
// revisions cache has a lower value than the object.revision, so update revision and add it to cache
if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev);
revisions.put(id, object.getRevision());
- cache.putForExternalRead(id, object);
+ if (lifespan < 0) cache.putForExternalRead(id, object);
+ else cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
} finally {
endRevisionBatch();
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java
index ed49ddf..22fef96 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java
@@ -1,5 +1,7 @@
package org.keycloak.models.cache.infinispan.entities;
+import org.keycloak.common.util.Time;
+
import java.io.Serializable;
/**
@@ -9,7 +11,7 @@ import java.io.Serializable;
public class AbstractRevisioned implements Revisioned, Serializable {
private String id;
private Long revision;
- private final long cacheTimestamp = System.currentTimeMillis();
+ private final long cacheTimestamp = Time.currentTimeMillis();
public AbstractRevisioned(Long revision, String id) {
this.revision = revision;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
index 547419e..72a1e77 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
@@ -20,9 +20,9 @@ package org.keycloak.models.cache.infinispan;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.constants.ServiceAccountConstants;
+import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
@@ -31,7 +31,6 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
-import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
@@ -43,8 +42,13 @@ import org.keycloak.models.cache.infinispan.entities.CachedUser;
import org.keycloak.models.cache.infinispan.entities.CachedUserConsent;
import org.keycloak.models.cache.infinispan.entities.CachedUserConsents;
import org.keycloak.models.cache.infinispan.entities.UserListQuery;
+import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@@ -177,22 +181,19 @@ public class UserCacheSession implements UserCache {
}
CachedUser cached = cache.get(id, CachedUser.class);
- UserModel delegate = null;
- boolean wasCached = cached != null;
+ UserModel adapter = null;
if (cached == null) {
logger.trace("not cached");
Long loaded = cache.getCurrentRevision(id);
- delegate = getDelegate().getUserById(id, realm);
+ UserModel delegate = getDelegate().getUserById(id, realm);
if (delegate == null) {
logger.trace("delegate returning null");
return null;
}
- cached = new CachedUser(loaded, realm, delegate);
- cache.addRevisioned(cached, startupRevision);
+ adapter = cacheUser(realm, delegate, loaded);
+ } else {
+ adapter = validateCache(realm, cached);
}
- logger.trace("returning new cache adapter");
- UserAdapter adapter = new UserAdapter(cached, this, session, realm);
- if (!wasCached) onCache(realm, adapter, delegate);
managedUsers.put(id, adapter);
return adapter;
}
@@ -238,15 +239,17 @@ public class UserCacheSession implements UserCache {
return null;
}
userId = model.getId();
- query = new UserListQuery(loaded, cacheKey, realm, model.getId());
- cache.addRevisioned(query, startupRevision);
if (invalidations.contains(userId)) return model;
if (managedUsers.containsKey(userId)) {
logger.tracev("return managed user");
return managedUsers.get(userId);
}
- UserAdapter adapter = getUserAdapter(realm, userId, loaded, model);
+ 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);
+ }
managedUsers.put(userId, adapter);
return adapter;
} else {
@@ -261,21 +264,132 @@ public class UserCacheSession implements UserCache {
}
}
- protected UserAdapter getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel delegate) {
+ protected UserModel getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel delegate) {
CachedUser cached = cache.get(userId, CachedUser.class);
- boolean wasCached = cached != null;
if (cached == null) {
- cached = new CachedUser(loaded, realm, delegate);
+ return cacheUser(realm, delegate, loaded);
+ } else {
+ return validateCache(realm, cached);
+ }
+ }
+
+ protected UserModel validateCache(RealmModel realm, CachedUser cached) {
+ StorageId storageId = new StorageId(cached.getId());
+ if (!storageId.isLocal()) {
+ ComponentModel component = realm.getComponent(storageId.getProviderId());
+ UserStorageProviderModel model = new UserStorageProviderModel(component);
+ UserStorageProviderModel.CachePolicy policy = model.getCachePolicy();
+ // although we do set a timeout, Infinispan has no guarantees when the user will be evicted
+ // its also hard to test stuff
+ boolean invalidate = false;
+ if (policy != null) {
+ String currentTime = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(Time.currentTimeMillis()));
+ if (policy == UserStorageProviderModel.CachePolicy.NO_CACHE) {
+ invalidate = true;
+ } else if (cached.getCacheTimestamp() < model.getCacheInvalidBefore()) {
+ invalidate = true;
+ } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_DAILY) {
+ long dailyTimeout = dailyTimeout(model.getEvictionHour(), model.getEvictionMinute());
+ dailyTimeout = dailyTimeout - (24 * 60 * 60 * 1000);
+ //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(dailyTimeout));
+ //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp()));
+ if (cached.getCacheTimestamp() <= dailyTimeout) {
+ invalidate = true;
+ }
+ } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_WEEKLY) {
+ int oneWeek = 7 * 24 * 60 * 60 * 1000;
+ long weeklyTimeout = weeklyTimeout(model.getEvictionDay(), model.getEvictionHour(), model.getEvictionMinute());
+ long lastTimeout = weeklyTimeout - oneWeek;
+ String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(weeklyTimeout));
+ String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp()));
+ if (cached.getCacheTimestamp() <= lastTimeout) {
+ invalidate = true;
+ }
+ }
+ }
+ if (invalidate) {
+ registerUserInvalidation(realm, cached);
+ return getDelegate().getUserById(cached.getId(), realm);
+ }
+ }
+ return new UserAdapter(cached, this, session, realm);
+ }
+
+ protected UserModel cacheUser(RealmModel realm, UserModel delegate, Long revision) {
+ StorageId storageId = new StorageId(delegate.getId());
+ CachedUser cached = null;
+ if (!storageId.isLocal()) {
+ ComponentModel component = realm.getComponent(storageId.getProviderId());
+ UserStorageProviderModel model = new UserStorageProviderModel(component);
+ UserStorageProviderModel.CachePolicy policy = model.getCachePolicy();
+ if (policy != null && policy == UserStorageProviderModel.CachePolicy.NO_CACHE) {
+ return delegate;
+ }
+ cached = new CachedUser(revision, realm, delegate);
+ if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) {
+ cache.addRevisioned(cached, startupRevision);
+ } else {
+ long lifespan = -1;
+ if (policy == UserStorageProviderModel.CachePolicy.EVICT_DAILY) {
+ if (model.getEvictionHour() > -1 && model.getEvictionMinute() > -1) {
+ lifespan = dailyTimeout(model.getEvictionHour(), model.getEvictionMinute()) - Time.currentTimeMillis();
+ }
+ } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_WEEKLY) {
+ if (model.getEvictionDay() > 0 && model.getEvictionHour() > -1 && model.getEvictionMinute() > -1) {
+ lifespan = weeklyTimeout(model.getEvictionDay(), model.getEvictionHour(), model.getEvictionMinute()) - Time.currentTimeMillis();
+ }
+ } else if (policy == UserStorageProviderModel.CachePolicy.MAX_LIFESPAN) {
+ lifespan = model.getMaxLifespan();
+ }
+ if (lifespan > 0) {
+ cache.addRevisioned(cached, startupRevision, lifespan);
+ } else {
+ cache.addRevisioned(cached, startupRevision);
+ }
+ }
+ } else {
+ cached = new CachedUser(revision, realm, delegate);
cache.addRevisioned(cached, startupRevision);
}
UserAdapter adapter = new UserAdapter(cached, this, session, realm);
- if (!wasCached) {
- onCache(realm, adapter, delegate);
- }
+ onCache(realm, adapter, delegate);
return adapter;
}
+
+ public static long dailyTimeout(int hour, int minute) {
+ Calendar cal = Calendar.getInstance();
+ Calendar cal2 = Calendar.getInstance();
+ cal.setTimeInMillis(Time.currentTimeMillis());
+ cal2.setTimeInMillis(Time.currentTimeMillis());
+ cal2.set(Calendar.HOUR_OF_DAY, hour);
+ cal2.set(Calendar.MINUTE, minute);
+ if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
+ int add = (24 * 60 * 60 * 1000);
+ cal.add(Calendar.MILLISECOND, add);
+ } else {
+ cal.add(Calendar.MILLISECOND, (int)(cal2.getTimeInMillis() - cal.getTimeInMillis()));
+ }
+ return cal.getTimeInMillis();
+ }
+
+ public static long weeklyTimeout(int day, int hour, int minute) {
+ Calendar cal = Calendar.getInstance();
+ Calendar cal2 = Calendar.getInstance();
+ cal.setTimeInMillis(Time.currentTimeMillis());
+ cal2.setTimeInMillis(Time.currentTimeMillis());
+ cal2.set(Calendar.HOUR_OF_DAY, hour);
+ cal2.set(Calendar.MINUTE, minute);
+ cal2.set(Calendar.DAY_OF_WEEK, day);
+ if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
+ int add = (7 * 24 * 60 * 60 * 1000);
+ cal2.add(Calendar.MILLISECOND, add);
+ }
+
+ return cal2.getTimeInMillis();
+ }
+
private void onCache(RealmModel realm, UserAdapter adapter, UserModel delegate) {
((OnUserCache)getDelegate()).onCache(realm, adapter, delegate);
((OnUserCache)session.userCredentialManager()).onCache(realm, adapter, delegate);
@@ -300,12 +414,14 @@ public class UserCacheSession implements UserCache {
UserModel model = getDelegate().getUserByEmail(email, realm);
if (model == null) return null;
userId = model.getId();
- query = new UserListQuery(loaded, cacheKey, realm, model.getId());
- cache.addRevisioned(query, startupRevision);
if (invalidations.contains(userId)) return model;
if (managedUsers.containsKey(userId)) return managedUsers.get(userId);
- UserAdapter adapter = getUserAdapter(realm, userId, loaded, model);
+ UserModel adapter = getUserAdapter(realm, userId, loaded, model);
+ if (adapter instanceof UserAdapter) {
+ query = new UserListQuery(loaded, cacheKey, realm, model.getId());
+ cache.addRevisioned(query, startupRevision);
+ }
managedUsers.put(userId, adapter);
return adapter;
} else {
@@ -343,12 +459,15 @@ public class UserCacheSession implements UserCache {
UserModel model = getDelegate().getUserByFederatedIdentity(socialLink, realm);
if (model == null) return null;
userId = model.getId();
- query = new UserListQuery(loaded, cacheKey, realm, userId);
- cache.addRevisioned(query, startupRevision);
if (invalidations.contains(userId)) return model;
if (managedUsers.containsKey(userId)) return managedUsers.get(userId);
- UserAdapter adapter = getUserAdapter(realm, userId, loaded, model);
+ UserModel adapter = getUserAdapter(realm, userId, loaded, model);
+ if (adapter instanceof UserAdapter) {
+ query = new UserListQuery(loaded, cacheKey, realm, model.getId());
+ cache.addRevisioned(query, startupRevision);
+ }
+
managedUsers.put(userId, adapter);
return adapter;
} else {
diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java
index 915eb02..4463795 100644
--- a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java
@@ -19,7 +19,11 @@ package org.keycloak.models.sessions.infinispan.initializer;
import org.junit.Assert;
import org.junit.Test;
+import org.keycloak.models.cache.infinispan.UserCacheSession;
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
import java.util.List;
/**
@@ -59,4 +63,31 @@ public class InitializerStateTest {
Assert.assertTrue(segments.contains(i));
}
}
+
+ @Test
+ public void testDailyTimeout() throws Exception {
+ Date date = new Date(UserCacheSession.dailyTimeout(10, 30));
+ System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
+ date = new Date(UserCacheSession.dailyTimeout(17, 45));
+ System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
+ date = new Date(UserCacheSession.weeklyTimeout(Calendar.MONDAY, 13, 45));
+ System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
+ date = new Date(UserCacheSession.weeklyTimeout(Calendar.THURSDAY, 13, 45));
+ System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
+ System.out.println("----");
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.HOUR, 1);
+ int hour = cal.get(Calendar.HOUR_OF_DAY);
+ int min = cal.get(Calendar.MINUTE);
+ date = new Date(cal.getTimeInMillis());
+ System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
+ date = new Date(UserCacheSession.dailyTimeout(hour, min));
+ System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
+ cal = Calendar.getInstance();
+ cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+ date = new Date(cal.getTimeInMillis());
+ System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
+
+
+ }
}
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 2317352..139e309 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -810,6 +810,8 @@ public class ModelToRepresentation {
} else {
config.put(e.getKey(), e.getValue());
}
+ } else {
+ config.put(e.getKey(), e.getValue());
}
}
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 8ae1caa..1205561 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1757,18 +1757,9 @@ public class RepresentationToModel {
component.setSubType(rep.getSubType());
}
- Map<String, ProviderConfigProperty> providerConfiguration = null;
- if (!internal) {
- providerConfiguration = ComponentUtil.getComponentConfigProperties(session, component);
- }
-
if (rep.getConfig() != null) {
Set<String> keys = new HashSet<>(rep.getConfig().keySet());
for (String k : keys) {
- if (!internal && !providerConfiguration.containsKey(k)) {
- break;
- }
-
List<String> values = rep.getConfig().get(k);
if (values == null || values.isEmpty() || values.get(0) == null || values.get(0).trim().isEmpty()) {
component.getConfig().remove(k);
diff --git a/server-spi/src/main/java/org/keycloak/storage/StorageId.java b/server-spi/src/main/java/org/keycloak/storage/StorageId.java
index 97bbc74..fbbc406 100644
--- a/server-spi/src/main/java/org/keycloak/storage/StorageId.java
+++ b/server-spi/src/main/java/org/keycloak/storage/StorageId.java
@@ -78,6 +78,10 @@ public class StorageId implements Serializable {
public static boolean isLocalStorage(String userId) {
return new StorageId(userId).getProviderId() == null;
}
+ public boolean isLocal() {
+ return getProviderId() == null;
+
+ }
public String getId() {
return id;
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
index 4cd038b..3fd0791 100755
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
@@ -28,6 +28,14 @@ import org.keycloak.component.PrioritizedComponentModel;
*/
public class UserStorageProviderModel extends PrioritizedComponentModel {
+ public static enum CachePolicy {
+ NO_CACHE,
+ DEFAULT,
+ EVICT_DAILY,
+ EVICT_WEEKLY,
+ MAX_LIFESPAN
+ }
+
public UserStorageProviderModel() {
setProviderType(UserStorageProvider.class.getName());
}
@@ -40,6 +48,104 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
private transient Integer changedSyncPeriod;
private transient Integer lastSync;
private transient Boolean importEnabled;
+ private transient CachePolicy cachePolicy;
+ private transient long maxLifespan = -1;
+ private transient int evictionHour = -1;
+ private transient int evictionMinute = -1;
+ private transient int evictionDay = -1;
+ private transient long cacheInvalidBefore = -1;
+
+ public CachePolicy getCachePolicy() {
+ if (cachePolicy == null) {
+ String str = getConfig().getFirst("cachePolicy");
+ if (str == null) return null;
+ cachePolicy = CachePolicy.valueOf(str);
+ }
+ return cachePolicy;
+ }
+
+ public void setCachePolicy(CachePolicy cachePolicy) {
+ this.cachePolicy = cachePolicy;
+ if (cachePolicy == null) {
+ getConfig().remove("cachePolicy");
+
+ } else {
+ getConfig().putSingle("cachePolicy", cachePolicy.name());
+ }
+ }
+
+ public long getMaxLifespan() {
+ if (maxLifespan < 0) {
+ String str = getConfig().getFirst("maxLifespan");
+ if (str == null) return -1;
+ maxLifespan = Long.valueOf(str);
+ }
+ return maxLifespan;
+ }
+
+ public void setMaxLifespan(long maxLifespan) {
+ this.maxLifespan = maxLifespan;
+ getConfig().putSingle("maxLifespan", Long.toString(maxLifespan));
+ }
+
+ public int getEvictionHour() {
+ if (evictionHour < 0) {
+ String str = getConfig().getFirst("evictionHour");
+ if (str == null) return -1;
+ evictionHour = Integer.valueOf(str);
+ }
+ return evictionHour;
+ }
+
+ public void setEvictionHour(int evictionHour) {
+ if (evictionHour > 23 || evictionHour < 0) throw new IllegalArgumentException("Must be between 0 and 23");
+ this.evictionHour = evictionHour;
+ getConfig().putSingle("evictionHour", Integer.toString(evictionHour));
+ }
+
+ public int getEvictionMinute() {
+ if (evictionMinute < 0) {
+ String str = getConfig().getFirst("evictionMinute");
+ if (str == null) return -1;
+ evictionMinute = Integer.valueOf(str);
+ }
+ return evictionMinute;
+ }
+
+ public void setEvictionMinute(int evictionMinute) {
+ if (evictionMinute > 59 || evictionMinute < 0) throw new IllegalArgumentException("Must be between 0 and 59");
+ this.evictionMinute = evictionMinute;
+ getConfig().putSingle("evictionMinute", Integer.toString(evictionMinute));
+ }
+
+ public int getEvictionDay() {
+ if (evictionDay < 0) {
+ String str = getConfig().getFirst("evictionDay");
+ if (str == null) return -1;
+ evictionDay = Integer.valueOf(str);
+ }
+ return evictionDay;
+ }
+
+ public void setEvictionDay(int evictionDay) {
+ if (evictionDay > 7 || evictionDay < 1) throw new IllegalArgumentException("Must be between 1 and 7");
+ this.evictionDay = evictionDay;
+ getConfig().putSingle("evictionDay", Integer.toString(evictionDay));
+ }
+
+ public long getCacheInvalidBefore() {
+ if (cacheInvalidBefore < 0) {
+ String str = getConfig().getFirst("cacheInvalidBefore");
+ if (str == null) return -1;
+ cacheInvalidBefore = Long.valueOf(str);
+ }
+ return cacheInvalidBefore;
+ }
+
+ public void setCacheInvalidBefore(long cacheInvalidBefore) {
+ this.cacheInvalidBefore = cacheInvalidBefore;
+ getConfig().putSingle("cacheInvalidBefore", Long.toString(cacheInvalidBefore));
+ }
public boolean isImportEnabled() {
if (importEnabled == null) {
@@ -54,6 +160,8 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
}
+
+
public void setImportEnabled(boolean flag) {
importEnabled = flag;
getConfig().putSingle("importEnabled", Boolean.toString(flag));
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
index 5a0e817..39e2bae 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
@@ -17,6 +17,7 @@
package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.common.ClientConnection;
import org.keycloak.component.ComponentModel;
@@ -88,6 +89,7 @@ public class ComponentResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
+ @NoCache
public List<ComponentRepresentation> getComponents(@QueryParam("parent") String parent, @QueryParam("type") String type) {
auth.requireView();
List<ComponentModel> components = Collections.EMPTY_LIST;
@@ -129,13 +131,15 @@ public class ComponentResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
+ @NoCache
public ComponentRepresentation getComponent(@PathParam("id") String id) {
auth.requireManage();
ComponentModel model = realm.getComponent(id);
if (model == null) {
throw new NotFoundException("Could not find component");
}
- return ModelToRepresentation.toRepresentation(session, model, false);
+ ComponentRepresentation rep = ModelToRepresentation.toRepresentation(session, model, false);
+ return rep;
}
@PUT
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
index 1f6dd10..6e79d69 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
@@ -16,11 +16,13 @@
*/
package org.keycloak.testsuite.federation.storage;
+import org.junit.After;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
+import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
@@ -28,6 +30,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.cache.infinispan.UserAdapter;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.storage.StorageId;
@@ -40,6 +43,7 @@ import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
+import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -122,6 +126,126 @@ public class UserStorageTest {
loginBadPassword("tbrady");
}
+ @After
+ public void resetTimeoffset() {
+ Time.setOffset(0);
+
+ }
+
+ @Test
+ public void testIDE() throws Exception {
+ Thread.sleep(100000000);
+ }
+
+ @Test
+ public void testDailyEviction() {
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.HOUR, 1);
+ int hour = cal.get(Calendar.HOUR_OF_DAY);
+ int min = cal.get(Calendar.MINUTE);
+
+ UserStorageProviderModel model = new UserStorageProviderModel(writableProvider);
+ model.setCachePolicy(UserStorageProviderModel.CachePolicy.EVICT_DAILY);
+ model.setEvictionHour(cal.get(Calendar.HOUR_OF_DAY));
+ model.setEvictionMinute(cal.get(Calendar.MINUTE));
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.realms().getRealmByName("test");
+ CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+ long thorTimestamp = thor.getCacheTimestamp();
+ realm.updateComponent(model);
+ keycloakRule.stopSession(session, true);
+
+ Time.setOffset(60 * 2 * 60); // 2 hours
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("test");
+ UserModel thor2 = session.users().getUserByUsername("thor", realm);
+ Assert.assertFalse(thor2 instanceof CachedUserModel);
+ model.getConfig().remove("cachePolicy");
+ model.getConfig().remove("evictionHour");
+ model.getConfig().remove("evictionMinute");
+ realm.updateComponent(model);
+ keycloakRule.stopSession(session, true);
+ }
+
+ @Test
+ public void testWeeklyEviction() {
+ Calendar cal = Calendar.getInstance();
+
+ // sets day of the week to 4 days from now
+ cal.add(Calendar.HOUR, 4 * 24);
+
+ UserStorageProviderModel model = new UserStorageProviderModel(writableProvider);
+ model.setCachePolicy(UserStorageProviderModel.CachePolicy.EVICT_WEEKLY);
+ model.setEvictionDay(cal.get(Calendar.DAY_OF_WEEK));
+ model.setEvictionHour(cal.get(Calendar.HOUR_OF_DAY));
+ model.setEvictionMinute(cal.get(Calendar.MINUTE));
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.realms().getRealmByName("test");
+ CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+ realm.updateComponent(model);
+ keycloakRule.stopSession(session, true);
+
+ Time.setOffset(60 * 60 * 24 * 2); // 2 days in future, should be cached still
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("test");
+ // test still
+ UserModel thor2 = session.users().getUserByUsername("thor", realm);
+ Assert.assertTrue(thor2 instanceof CachedUserModel);
+ keycloakRule.stopSession(session, true);
+ Time.setOffset(Time.getOffset() + 60 * 60 * 24 * 3); // 3 days into future, cache will be invalidated
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("test");
+ thor2 = session.users().getUserByUsername("thor", realm);
+ Assert.assertFalse(thor2 instanceof CachedUserModel);
+ model.getConfig().remove("cachePolicy");
+ model.getConfig().remove("evictionHour");
+ model.getConfig().remove("evictionMinute");
+ model.getConfig().remove("evictionDay");
+ realm.updateComponent(model);
+ keycloakRule.stopSession(session, true);
+ }
+
+ @Test
+ public void testNoCache() {
+ UserStorageProviderModel model = new UserStorageProviderModel(writableProvider);
+ model.setCachePolicy(UserStorageProviderModel.CachePolicy.NO_CACHE);
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.realms().getRealmByName("test");
+ CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+ realm.updateComponent(model);
+ keycloakRule.stopSession(session, true);
+
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("test");
+ // test still
+ UserModel thor2 = session.users().getUserByUsername("thor", realm);
+ Assert.assertFalse(thor2 instanceof CachedUserModel);
+ keycloakRule.stopSession(session, true);
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("test");
+ thor2 = session.users().getUserByUsername("thor", realm);
+ Assert.assertFalse(thor2 instanceof CachedUserModel);
+ model.getConfig().remove("cachePolicy");
+ model.getConfig().remove("evictionHour");
+ model.getConfig().remove("evictionMinute");
+ model.getConfig().remove("evictionDay");
+ realm.updateComponent(model);
+ keycloakRule.stopSession(session, true);
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("test");
+ thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+ keycloakRule.stopSession(session, true);
+
+ }
+
@Test
public void testUpdate() {
KeycloakSession session = keycloakRule.startSession();
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 8956c33..4943c38 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1178,4 +1178,31 @@ keystores=Keystores
add-keystore=Add Keystore
add-keystore.placeholder=Add keystore...
view=View
-active=Active
\ No newline at end of file
+active=Active
+
+Sunday=Sunday
+Monday=Monday
+Tuesday=Tuesday
+Wednesday=Wednesday
+Thursday=Thursday
+Friday=Friday
+Saturday=Saturday
+
+user-strage-cache=Cache Settings
+userStorage.cachePolicy=Cache Policy
+userStorage.cachePolicy.option.DEFAULT=DEFAULT
+userStorage.cachePolicy.option.EVICT_WEEKLY=EVICT_WEEKLY
+userStorage.cachePolicy.option.EVICT_DAILY=EVICT_DAILY
+userStorage.cachePolicy.option.MAX_LIFESPAN=MAX_LIFESPAN
+userStorage.cachePolicy.option.NO_CACHE=NO_CACHE
+userStorage.cachePolicy.tooltip=Cache Policy for this storage provider. 'DEFAULT' is whatever the default settings are for the global user cache. 'EVICT_DAILY' is a time of day every day that the user cache will be invalidated. 'EVICT_WEEKLY' is a day of the week and time the cache will be invalidated. 'MAX-LIFESPAN' is the time in milliseconds that will be the lifespan of a cache entry.
+userStorage.cachePolicy.evictionDay=Eviction Day
+userStorage.cachePolicy.evictionDay.tooltip=Day of the week the entry will become invalid on
+userStorage.cachePolicy.evictionHour=Eviction Hour
+userStorage.cachePolicy.evictionHour.tooltip=Hour of day the entry will become invalid on.
+userStorage.cachePolicy.evictionMinute=Eviction Minute
+userStorage.cachePolicy.evictionMinute.tooltip=Minute of day the entry will become invalid on.
+userStorage.cachePolicy.maxLifespan=Max Lifespan
+userStorage.cachePolicy.maxLifespan.tooltip=Max lifespan of a user cache entry in milliseconds.
+
+
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index e5cf535..9ffe627 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -653,6 +653,9 @@ module.controller('UserFederationCtrl', function($scope, $location, $route, real
if (instance.isUserFederationProvider) {
return instance.priority;
} else {
+ if (!instance.config['priority']) {
+ console.log('getInstancePriority is undefined');
+ }
return instance.config['priority'][0];
}
}
@@ -740,6 +743,12 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
if (providerFactory.metadata.synchronizable) {
instance.config['fullSyncPeriod'] = ['-1'];
instance.config['changedSyncPeriod'] = ['-1'];
+ instance.config['cachePolicy'] = ['DEFAULT'];
+ instance.config['evictionDay'] = [''];
+ instance.config['evictionHour'] = [''];
+ instance.config['evictionMinute'] = [''];
+ instance.config['maxLifespan'] = [''];
+
}
if (providerFactory.properties) {
@@ -769,6 +778,27 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
}
}
+ if (!instance.config['cachePolicy']) {
+ instance.config['cachePolicy'] = ['DEFAULT'];
+
+ }
+ if (!instance.config['evictionDay']) {
+ instance.config['evictionDay'] = [''];
+
+ }
+ if (!instance.config['evictionHour']) {
+ instance.config['evictionHour'] = [''];
+
+ }
+ if (!instance.config['evictionMinute']) {
+ instance.config['evictionMinute'] = [''];
+
+ }
+ if (!instance.config['maxLifespan']) {
+ instance.config['maxLifespan'] = [''];
+
+ }
+
/*
console.log('Manage instance');
console.log(instance.name);
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
index 68b6489..e9d7202 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
@@ -68,6 +68,153 @@
</fieldset>
+ <fieldset>
+ <legend><span class="text">{{:: 'user-storage-cache-policy' | translate}}</span></legend>
+ <div class="form-group">
+ <label for="cachePolicy" class="col-md-2 control-label">{{:: 'userStorage.cachePolicy' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="cachePolicy" ng-model="instance.config['cachePolicy'][0]" class="form-control">
+ <option value="DEFAULT">{{:: 'userStorage.cachePolicy.option.DEFAULT' | translate}}</option>
+ <option value="EVICT_DAILY">{{:: 'userStorage.cachePolicy.option.EVICT_DAILY' | translate}}</option>
+ <option value="EVICT_WEEKLY">{{:: 'userStorage.cachePolicy.option.EVICT_WEEKLY' | translate}}</option>
+ <option value="MAX_LIFESPAN">{{:: 'userStorage.cachePolicy.option.MAX_LIFESPAN' | translate}}</option>
+ <option value="NO_CACHE">{{:: 'userStorage.cachePolicy.option.NO_CACHE' | translate}}</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'userStorage.cachePolicy.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY'">
+ <label for="evictionDay" class="col-md-2 control-label">{{:: 'userStorage.evictionDay' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="evictionDay" ng-model="instance.config['evictionDay'][0]" class="form-control">
+ <option value="1">{{:: 'Sunday' | translate}}</option>
+ <option value="2">{{:: 'Monday' | translate}}</option>
+ <option value="3">{{:: 'Tuesday' | translate}}</option>
+ <option value="4">{{:: 'Wednesday' | translate}}</option>
+ <option value="5">{{:: 'Thursday' | translate}}</option>
+ <option value="6">{{:: 'Friday' | translate}}</option>
+ <option value="7">{{:: 'Saturday' | translate}}</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'userStorage.cachePolicy.evictionDay.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY' || instance.config['cachePolicy'][0] == 'EVICT_DAILY'">
+ <label class="col-md-2 control-label" for="evictionHour">{{:: 'userStorage.cachePolicy.evictionHour' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="evictionHour" ng-model="instance.config['evictionHour'][0]" class="form-control">
+ <option value="0">00</option>
+ <option value="1">01</option>
+ <option value="2">02</option>
+ <option value="3">03</option>
+ <option value="4">04</option>
+ <option value="5">05</option>
+ <option value="6">06</option>
+ <option value="7">07</option>
+ <option value="8">08</option>
+ <option value="9">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ <option value="16">16</option>
+ <option value="17">17</option>
+ <option value="18">18</option>
+ <option value="19">19</option>
+ <option value="20">20</option>
+ <option value="21">21</option>
+ <option value="22">22</option>
+ <option value="23">23</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'userStorage.cachePolicy.evictionHour.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY' || instance.config['cachePolicy'][0] == 'EVICT_DAILY'">
+ <label class="col-md-2 control-label" for="evictionMinute">{{:: 'userStorage.cachePolicy.evictionMinute' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="evictionMinute" ng-model="instance.config['evictionMinute'][0]" class="form-control">
+ <option value="0">00</option>
+ <option value="1">01</option>
+ <option value="2">02</option>
+ <option value="3">03</option>
+ <option value="4">04</option>
+ <option value="5">05</option>
+ <option value="6">06</option>
+ <option value="7">07</option>
+ <option value="8">08</option>
+ <option value="9">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ <option value="16">16</option>
+ <option value="17">17</option>
+ <option value="18">18</option>
+ <option value="19">19</option>
+ <option value="20">20</option>
+ <option value="21">21</option>
+ <option value="22">22</option>
+ <option value="23">23</option>
+ <option value="24">24</option>
+ <option value="25">25</option>
+ <option value="26">26</option>
+ <option value="27">27</option>
+ <option value="28">28</option>
+ <option value="29">29</option>
+ <option value="30">30</option>
+ <option value="31">31</option>
+ <option value="32">32</option>
+ <option value="33">33</option>
+ <option value="34">34</option>
+ <option value="35">35</option>
+ <option value="36">36</option>
+ <option value="37">37</option>
+ <option value="38">38</option>
+ <option value="39">39</option>
+ <option value="40">40</option>
+ <option value="41">41</option>
+ <option value="42">42</option>
+ <option value="43">43</option>
+ <option value="44">44</option>
+ <option value="45">45</option>
+ <option value="46">46</option>
+ <option value="47">47</option>
+ <option value="48">48</option>
+ <option value="49">49</option>
+ <option value="50">50</option>
+ <option value="51">51</option>
+ <option value="52">52</option>
+ <option value="53">53</option>
+ <option value="54">54</option>
+ <option value="55">55</option>
+ <option value="56">56</option>
+ <option value="57">57</option>
+ <option value="58">58</option>
+ <option value="59">59</option>
+ </select>
+ </div>
+ <kc-tooltip>{{:: 'userStorage.cachePolicy.evictionMinute.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'MAX_LIFESPAN'">
+ <label class="col-md-2 control-label" for="maxLifespan">{{:: 'userStorage.cachePolicy.maxLifespan' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" type="text" ng-model="instance.config['maxLifespan'][0]" id="maxLifespan" />
+ </div>
+ <kc-tooltip>{{:: 'userStorage.cachePolicy.maxLifespan.tooltip' | translate}}</kc-tooltip>
+ </div>
+ </fieldset>
+
+
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageUsers">
<button kc-save>{{:: 'save' | translate}}</button>