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 f077927861..ad1ba26fab 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/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 547419e329..72a1e77118 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/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 ed49ddff1e..22fef962c4 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/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 915eb02efd..44637953f1 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 23173528db..139e309a1f 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 8ae1caa7e0..1205561dc7 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 providerConfiguration = null; - if (!internal) { - providerConfiguration = ComponentUtil.getComponentConfigProperties(session, component); - } - if (rep.getConfig() != null) { Set keys = new HashSet<>(rep.getConfig().keySet()); for (String k : keys) { - if (!internal && !providerConfiguration.containsKey(k)) { - break; - } - List 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 97bbc747a1..fbbc406857 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 4cd038baa1..3fd0791853 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 5a0e817af1..39e2bae346 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 getComponents(@QueryParam("parent") String parent, @QueryParam("type") String type) { auth.requireView(); List 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 1f6dd105ec..6e79d69aa1 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 8956c33a91..4943c38469 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 e5cf535af3..9ffe627056 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 68b64890c0..e9d7202215 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 @@ +
+ {{:: 'user-storage-cache-policy' | translate}} +
+ +
+
+ +
+
+ {{:: 'userStorage.cachePolicy.tooltip' | translate}} +
+
+ +
+
+ +
+
+ {{:: 'userStorage.cachePolicy.evictionDay.tooltip' | translate}} +
+
+ +
+
+ +
+
+ {{:: 'userStorage.cachePolicy.evictionHour.tooltip' | translate}} +
+
+ +
+
+ +
+ {{:: 'userStorage.cachePolicy.evictionMinute.tooltip' | translate}} +
+
+ +
+ +
+ {{:: 'userStorage.cachePolicy.maxLifespan.tooltip' | translate}} +
+
+ +