keycloak-uncached

user spi cache policy

10/24/2016 5:36:37 PM

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>