From 4bf23cc83a7d5779d6313760c7765750a708a931 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Mon, 29 Jan 2018 12:28:17 -0500 Subject: [PATCH] caching --- .../cache/infinispan/ClientAdapter.java | 4 +- .../models/cache/infinispan/RealmAdapter.java | 33 +-- .../models/cache/infinispan/RealmCache.java | 81 -------- .../cache/infinispan/RealmCacheSession.java | 95 +++++++-- .../cache/infinispan/UserCacheSession.java | 98 +-------- .../entities/AbstractRevisioned.java | 4 +- .../initializer/InitializerStateTest.java | 11 +- .../keycloak/models/cache/CachedObject.java | 25 +++ .../CacheableStorageProviderModel.java | 117 +++++++++++ .../storage/UserStorageProviderModel.java | 21 -- .../federation/storage/ClientStorageTest.java | 189 +++++++++++++++++- 11 files changed, 445 insertions(+), 233 deletions(-) delete mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCache.java create mode 100644 server-spi/src/main/java/org/keycloak/models/cache/CachedObject.java diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index 01cfd8c015..3176506743 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -39,14 +39,12 @@ import java.util.Set; public class ClientAdapter implements ClientModel { protected RealmCacheSession cacheSession; protected RealmModel cachedRealm; - protected RealmCache cache; protected ClientModel updated; protected CachedClient cached; - public ClientAdapter(RealmModel cachedRealm, CachedClient cached, RealmCacheSession cacheSession, RealmCache cache) { + public ClientAdapter(RealmModel cachedRealm, CachedClient cached, RealmCacheSession cacheSession) { this.cachedRealm = cachedRealm; - this.cache = cache; this.cacheSession = cacheSession; this.cached = cached; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index b80bbe385c..17cacc8b4a 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -24,6 +24,7 @@ import org.keycloak.models.*; import org.keycloak.models.cache.CachedRealmModel; import org.keycloak.models.cache.infinispan.entities.CachedRealm; import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.client.ClientStorageProvider; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -36,7 +37,6 @@ public class RealmAdapter implements CachedRealmModel { protected CachedRealm cached; protected RealmCacheSession cacheSession; protected volatile RealmModel updated; - protected RealmCache cache; protected KeycloakSession session; public RealmAdapter(KeycloakSession session, CachedRealm cached, RealmCacheSession cacheSession) { @@ -1323,35 +1323,37 @@ public class RealmAdapter implements CachedRealmModel { @Override public ComponentModel addComponentModel(ComponentModel model) { getDelegateForUpdate(); - evictUsers(model); + executeEvictions(model); return updated.addComponentModel(model); } @Override public ComponentModel importComponentModel(ComponentModel model) { getDelegateForUpdate(); - evictUsers(model); + executeEvictions(model); return updated.importComponentModel(model); } - public void evictUsers(ComponentModel model) { - String parentId = model.getParentId(); - evictUsers(parentId); - } + public void executeEvictions(ComponentModel model) { + if (model == null) return; + // test that this is a realm component + if (model.getParentId() != null && !model.getParentId().equals(getId())) return; - public void evictUsers(String parentId) { - if (parentId != null && !parentId.equals(getId())) { - ComponentModel parent = getComponent(parentId); - if (parent != null && UserStorageProvider.class.getName().equals(parent.getProviderType())) { - session.userCache().evict(this); - } + // invalidate entire user cache if we're dealing with user storage SPI + if (UserStorageProvider.class.getName().equals(model.getProviderType())) { + session.userCache().evict(this); + } + // invalidate entire realm if we're dealing with client storage SPI + // entire realm because of client roles, client lists, and clients + if (ClientStorageProvider.class.getName().equals(model.getProviderType())) { + cacheSession.evictRealmOnRemoval(this); } } @Override public void updateComponent(ComponentModel component) { getDelegateForUpdate(); - evictUsers(component); + executeEvictions(component); updated.updateComponent(component); } @@ -1359,7 +1361,7 @@ public class RealmAdapter implements CachedRealmModel { @Override public void removeComponent(ComponentModel component) { getDelegateForUpdate(); - evictUsers(component); + executeEvictions(component); updated.removeComponent(component); } @@ -1367,7 +1369,6 @@ public class RealmAdapter implements CachedRealmModel { @Override public void removeComponents(String parentId) { getDelegateForUpdate(); - evictUsers(parentId); updated.removeComponents(parentId); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCache.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCache.java deleted file mode 100755 index d683bb015b..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCache.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.cache.infinispan; - -import org.keycloak.models.cache.infinispan.entities.CachedClient; -import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate; -import org.keycloak.models.cache.infinispan.entities.CachedGroup; -import org.keycloak.models.cache.infinispan.entities.CachedRealm; -import org.keycloak.models.cache.infinispan.entities.CachedRole; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface RealmCache { - void clear(); - - CachedRealm getRealm(String id); - - void invalidateRealm(CachedRealm realm); - - void addRealm(CachedRealm realm); - - CachedRealm getRealmByName(String name); - - void invalidateRealmById(String id); - - CachedClient getClient(String id); - - void invalidateClient(CachedClient app); - - void evictClientById(String id); - - void addClient(CachedClient app); - - void invalidateClientById(String id); - - CachedRole getRole(String id); - - void invalidateRole(CachedRole role); - - void evictRoleById(String id); - - void addRole(CachedRole role); - - void invalidateRoleById(String id); - - CachedGroup getGroup(String id); - - void invalidateGroup(CachedGroup role); - - void addGroup(CachedGroup role); - - void invalidateGroupById(String id); - - CachedClientTemplate getClientTemplate(String id); - - void invalidateClientTemplate(CachedClientTemplate app); - - void evictClientTemplateById(String id); - - void addClientTemplate(CachedClientTemplate app); - - void invalidateClientTemplateById(String id); - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 289e3a14e1..018213f6a8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.component.ComponentModel; import org.keycloak.migration.MigrationModel; import org.keycloak.models.*; import org.keycloak.models.cache.CacheRealmProvider; @@ -26,6 +27,9 @@ import org.keycloak.models.cache.CachedRealmModel; import org.keycloak.models.cache.infinispan.entities.*; import org.keycloak.models.cache.infinispan.events.*; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.CacheableStorageProviderModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.client.ClientStorageProviderModel; import java.util.*; @@ -100,7 +104,7 @@ public class RealmCacheSession implements CacheRealmProvider { protected boolean setRollbackOnly; protected Map managedRealms = new HashMap<>(); - protected Map managedApplications = new HashMap<>(); + protected Map managedApplications = new HashMap<>(); protected Map managedClientTemplates = new HashMap<>(); protected Map managedRoles = new HashMap<>(); protected Map managedGroups = new HashMap<>(); @@ -173,8 +177,8 @@ public class RealmCacheSession implements CacheRealmProvider { private void invalidateClient(String id) { invalidations.add(id); - ClientAdapter adapter = managedApplications.get(id); - if (adapter != null) adapter.invalidate(); + ClientModel adapter = managedApplications.get(id); + if (adapter != null && adapter instanceof ClientAdapter) ((ClientAdapter)adapter).invalidate(); } @Override @@ -204,9 +208,9 @@ public class RealmCacheSession implements CacheRealmProvider { invalidations.addAll(newInvalidations); // need to make sure that scope and group mapping clients and groups are invalidated for (String id : newInvalidations) { - ClientAdapter adapter = managedApplications.get(id); - if (adapter != null) { - adapter.invalidate(); + ClientModel adapter = managedApplications.get(id); + if (adapter != null && adapter instanceof ClientAdapter){ + ((ClientAdapter)adapter).invalidate(); continue; } GroupAdapter group = managedGroups.get(id); @@ -329,7 +333,6 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public void commit() { try { - if (realmDelegate == null) return; if (clearAll) { cache.clear(); } @@ -470,12 +473,16 @@ public class RealmCacheSession implements CacheRealmProvider { RealmModel realm = getRealm(id); if (realm == null) return false; - cache.invalidateObject(id); - invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName())); - cache.realmRemoval(id, realm.getName(), invalidations); + evictRealmOnRemoval(realm); return getRealmDelegate().removeRealm(id); } + public void evictRealmOnRemoval(RealmModel realm) { + cache.invalidateObject(realm.getId()); + invalidationEvents.add(RealmRemovedEvent.create(realm.getId(), realm.getName())); + cache.realmRemoval(realm.getId(), realm.getName(), invalidations); + } + @Override public ClientModel addClient(RealmModel realm, String clientId) { @@ -1020,20 +1027,78 @@ public class RealmCacheSession implements CacheRealmProvider { Long loaded = cache.getCurrentRevision(id); ClientModel model = getClientDelegate().getClientById(id, realm); if (model == null) return null; - if (invalidations.contains(id)) return model; - cached = new CachedClient(loaded, realm, model); - logger.tracev("adding client by id cache miss: {0}", cached.getClientId()); - cache.addRevisioned(cached, startupRevision); + ClientModel adapter = cacheClient(realm, model, loaded); + managedApplications.put(id, adapter); + return adapter; } else if (invalidations.contains(id)) { return getRealmDelegate().getClientById(id, realm); } else if (managedApplications.containsKey(id)) { return managedApplications.get(id); } - ClientAdapter adapter = new ClientAdapter(realm, cached, this, null); + ClientModel adapter = validateCache(realm, cached); managedApplications.put(id, adapter); return adapter; } + protected ClientModel cacheClient(RealmModel realm, ClientModel delegate, Long revision) { + if (invalidations.contains(delegate.getId())) return delegate; + StorageId storageId = new StorageId(delegate.getId()); + CachedClient cached = null; + ClientAdapter adapter = null; + + if (!storageId.isLocal()) { + ComponentModel component = realm.getComponent(storageId.getProviderId()); + ClientStorageProviderModel model = new ClientStorageProviderModel(component); + if (!model.isEnabled()) { + return delegate; + } + ClientStorageProviderModel.CachePolicy policy = model.getCachePolicy(); + if (policy != null && policy == ClientStorageProviderModel.CachePolicy.NO_CACHE) { + return delegate; + } + + cached = new CachedClient(revision, realm, delegate); + adapter = new ClientAdapter(realm, cached, this); + + long lifespan = model.getLifespan(); + if (lifespan > 0) { + cache.addRevisioned(cached, startupRevision, lifespan); + } else { + cache.addRevisioned(cached, startupRevision); + } + } else { + cached = new CachedClient(revision, realm, delegate); + adapter = new ClientAdapter(realm, cached, this); + cache.addRevisioned(cached, startupRevision); + } + + return adapter; + } + + + protected ClientModel validateCache(RealmModel realm, CachedClient cached) { + if (!realm.getId().equals(cached.getRealm())) { + return null; + } + + StorageId storageId = new StorageId(cached.getId()); + if (!storageId.isLocal()) { + ComponentModel component = realm.getComponent(storageId.getProviderId()); + ClientStorageProviderModel model = new ClientStorageProviderModel(component); + + // although we do set a timeout, Infinispan has no guarantees when the user will be evicted + // its also hard to test stuff + if (model.shouldInvalidate(cached)) { + registerClientInvalidation(cached.getId(), cached.getClientId(), realm.getId()); + return getClientDelegate().getClientById(cached.getId(), realm); + } + } + ClientAdapter adapter = new ClientAdapter(realm, cached, this); + + return adapter; + } + + @Override public ClientModel getClientByClientId(String clientId, RealmModel realm) { String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId()); 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 4b5309a613..e2fcc83f24 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 @@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.models.cache.CachedObject; import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.util.Time; @@ -49,6 +50,7 @@ import org.keycloak.models.cache.infinispan.events.UserFederationLinkUpdatedEven import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent; import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent; import org.keycloak.models.utils.ReadOnlyUserModelDelegate; +import org.keycloak.storage.CacheableStorageProviderModel; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; @@ -144,7 +146,6 @@ public class UserCacheSession implements UserCache { @Override public void commit() { - if (delegate == null) return; runInvalidations(); transactionActive = false; } @@ -296,46 +297,11 @@ public class UserCacheSession implements UserCache { if (!storageId.isLocal()) { ComponentModel component = realm.getComponent(storageId.getProviderId()); - UserStorageProviderModel model = new UserStorageProviderModel(component); + CacheableStorageProviderModel model = new CacheableStorageProviderModel(component); // 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 (!model.isEnabled()) { - invalidate = true; - } else { - UserStorageProviderModel.CachePolicy policy = model.getCachePolicy(); - 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.MAX_LIFESPAN) { - if (cached.getCacheTimestamp() + model.getMaxLifespan() < Time.currentTimeMillis()) { - 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) { + if (model.shouldInvalidate(cached)) { registerUserInvalidation(realm, cached); return getDelegate().getUserById(cached.getId(), realm); } @@ -371,26 +337,11 @@ public class UserCacheSession implements UserCache { adapter = new UserAdapter(cached, this, session, realm); onCache(realm, adapter, delegate); - if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) { - cache.addRevisioned(cached, startupRevision); + long lifespan = model.getLifespan(); + if (lifespan > 0) { + cache.addRevisioned(cached, startupRevision, lifespan); } 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); - } + cache.addRevisioned(cached, startupRevision); } } else { cached = new CachedUser(revision, realm, delegate, notBefore); @@ -402,39 +353,6 @@ public class UserCacheSession implements UserCache { 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); 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 22fef962c4..8c88df774f 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,6 +1,7 @@ package org.keycloak.models.cache.infinispan.entities; import org.keycloak.common.util.Time; +import org.keycloak.models.cache.CachedObject; import java.io.Serializable; @@ -8,7 +9,7 @@ import java.io.Serializable; * @author Bill Burke * @version $Revision: 1 $ */ -public class AbstractRevisioned implements Revisioned, Serializable { +public class AbstractRevisioned implements Revisioned, Serializable, CachedObject { private String id; private Long revision; private final long cacheTimestamp = Time.currentTimeMillis(); @@ -38,6 +39,7 @@ public class AbstractRevisioned implements Revisioned, Serializable { * * @return */ + @Override public long getCacheTimestamp() { return cacheTimestamp; } 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 32210a0f02..d26f2b99fe 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 @@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan.initializer; import org.junit.Assert; import org.junit.Test; import org.keycloak.models.cache.infinispan.UserCacheSession; +import org.keycloak.storage.CacheableStorageProviderModel; import java.text.DateFormat; import java.util.Calendar; @@ -65,13 +66,13 @@ public class InitializerStateTest { @Test public void testDailyTimeout() throws Exception { - Date date = new Date(UserCacheSession.dailyTimeout(10, 30)); + Date date = new Date(CacheableStorageProviderModel.dailyTimeout(10, 30)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); - date = new Date(UserCacheSession.dailyTimeout(17, 45)); + date = new Date(CacheableStorageProviderModel.dailyTimeout(17, 45)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); - date = new Date(UserCacheSession.weeklyTimeout(Calendar.MONDAY, 13, 45)); + date = new Date(CacheableStorageProviderModel.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)); + date = new Date(CacheableStorageProviderModel.weeklyTimeout(Calendar.THURSDAY, 13, 45)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); System.out.println("----"); Calendar cal = Calendar.getInstance(); @@ -80,7 +81,7 @@ public class InitializerStateTest { 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)); + date = new Date(CacheableStorageProviderModel.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); diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CachedObject.java b/server-spi/src/main/java/org/keycloak/models/cache/CachedObject.java new file mode 100644 index 0000000000..f252db55fa --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/cache/CachedObject.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.cache; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface CachedObject { + long getCacheTimestamp(); +} diff --git a/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java index 8263bfe153..dd740f234c 100644 --- a/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java +++ b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java @@ -16,8 +16,12 @@ */ package org.keycloak.storage; +import org.keycloak.common.util.Time; import org.keycloak.component.ComponentModel; import org.keycloak.component.PrioritizedComponentModel; +import org.keycloak.models.cache.CachedObject; + +import java.util.Calendar; /** * @author Bill Burke @@ -30,6 +34,7 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel { public static final String EVICTION_MINUTE = "evictionMinute"; public static final String EVICTION_DAY = "evictionDay"; public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore"; + public static final String ENABLED = "enabled"; private transient CachePolicy cachePolicy; private transient long maxLifespan = -1; @@ -37,6 +42,7 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel { private transient int evictionMinute = -1; private transient int evictionDay = -1; private transient long cacheInvalidBefore = -1; + private transient Boolean enabled; public CacheableStorageProviderModel() { } @@ -137,6 +143,117 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel { getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore)); } + public void setEnabled(boolean flag) { + enabled = flag; + getConfig().putSingle(ENABLED, Boolean.toString(flag)); + } + + public boolean isEnabled() { + if (enabled == null) { + String val = getConfig().getFirst(ENABLED); + if (val == null) { + enabled = true; + } else { + enabled = Boolean.valueOf(val); + } + } + return enabled; + + } + + public long getLifespan() { + UserStorageProviderModel.CachePolicy policy = getCachePolicy(); + long lifespan = -1; + if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) { + lifespan = -1; + } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_DAILY) { + if (getEvictionHour() > -1 && getEvictionMinute() > -1) { + lifespan = dailyTimeout(getEvictionHour(), getEvictionMinute()) - Time.currentTimeMillis(); + } + } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY) { + if (getEvictionDay() > 0 && getEvictionHour() > -1 && getEvictionMinute() > -1) { + lifespan = weeklyTimeout(getEvictionDay(), getEvictionHour(), getEvictionMinute()) - Time.currentTimeMillis(); + } + } else if (policy == CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN) { + lifespan = getMaxLifespan(); + } + return lifespan; + } + + public boolean shouldInvalidate(CachedObject cached) { + boolean invalidate = false; + if (!isEnabled()) { + invalidate = true; + } else { + CacheableStorageProviderModel.CachePolicy policy = getCachePolicy(); + if (policy != null) { + //String currentTime = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(Time.currentTimeMillis())); + if (policy == CacheableStorageProviderModel.CachePolicy.NO_CACHE) { + invalidate = true; + } else if (cached.getCacheTimestamp() < getCacheInvalidBefore()) { + invalidate = true; + } else if (policy == CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN) { + if (cached.getCacheTimestamp() + getMaxLifespan() < Time.currentTimeMillis()) { + invalidate = true; + } + } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_DAILY) { + long dailyTimeout = dailyTimeout(getEvictionHour(), 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 == CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY) { + int oneWeek = 7 * 24 * 60 * 60 * 1000; + long weeklyTimeout = weeklyTimeout(getEvictionDay(), getEvictionHour(), 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; + } + } + } + } + return invalidate; + } + + + 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(); + } + + + public enum CachePolicy { NO_CACHE, DEFAULT, 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 0b32b94c25..e145c40225 100755 --- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java +++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java @@ -18,7 +18,6 @@ package org.keycloak.storage; import org.keycloak.component.ComponentModel; -import org.keycloak.component.PrioritizedComponentModel; /** * Stored configuration of a User Storage provider instance. @@ -32,7 +31,6 @@ public class UserStorageProviderModel extends CacheableStorageProviderModel { public static final String FULL_SYNC_PERIOD = "fullSyncPeriod"; public static final String CHANGED_SYNC_PERIOD = "changedSyncPeriod"; public static final String LAST_SYNC = "lastSync"; - public static final String ENABLED = "enabled"; public UserStorageProviderModel() { setProviderType(UserStorageProvider.class.getName()); @@ -46,7 +44,6 @@ public class UserStorageProviderModel extends CacheableStorageProviderModel { private transient Integer changedSyncPeriod; private transient Integer lastSync; private transient Boolean importEnabled; - private transient Boolean enabled; public boolean isImportEnabled() { if (importEnabled == null) { @@ -66,24 +63,6 @@ public class UserStorageProviderModel extends CacheableStorageProviderModel { getConfig().putSingle(IMPORT_ENABLED, Boolean.toString(flag)); } - public void setEnabled(boolean flag) { - enabled = flag; - getConfig().putSingle(ENABLED, Boolean.toString(flag)); - } - - - public boolean isEnabled() { - if (enabled == null) { - String val = getConfig().getFirst(ENABLED); - if (val == null) { - enabled = true; - } else { - enabled = Boolean.valueOf(val); - } - } - return enabled; - - } public int getFullSyncPeriod() { if (fullSyncPeriod == null) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java index a1af7fe449..c8c2f529f1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java @@ -30,17 +30,21 @@ import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; import org.keycloak.events.Details; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowBindings; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.cache.infinispan.ClientAdapter; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.storage.CacheableStorageProviderModel; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.storage.client.ClientStorageProviderModel; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; @@ -66,9 +70,18 @@ import javax.ws.rs.core.Response; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; +import java.util.Calendar; import java.util.List; +import static java.util.Calendar.DAY_OF_WEEK; +import static java.util.Calendar.HOUR_OF_DAY; +import static java.util.Calendar.MINUTE; import static org.junit.Assert.assertEquals; +import static org.keycloak.storage.CacheableStorageProviderModel.CACHE_POLICY; +import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_DAY; +import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_HOUR; +import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_MINUTE; +import static org.keycloak.storage.CacheableStorageProviderModel.MAX_LIFESPAN; /** * Test that clients can override auth flows @@ -92,6 +105,8 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest { public void configureTestRealm(RealmRepresentation testRealm) { } + protected String providerId; + @Deployment public static WebArchive deploy() { return RunOnServerDeployment.create(UserResource.class) @@ -116,7 +131,7 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest { provider.getConfig().putSingle(HardcodedClientStorageProviderFactory.CLIENT_ID, "hardcoded-client"); provider.getConfig().putSingle(HardcodedClientStorageProviderFactory.REDIRECT_URI, oauth.getRedirectUri()); - String providerId = addComponent(provider); + providerId = addComponent(provider); } @@ -212,4 +227,176 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest { httpClient.close(); events.clear(); } + + /* + + @Test + public void testDailyEviction() { + + // set eviction to 1 hour from now + Calendar eviction = Calendar.getInstance(); + eviction.add(Calendar.HOUR, 1); + ComponentRepresentation propProviderRW = testRealmResource().components().component(propProviderRWId).toRepresentation(); + propProviderRW.getConfig().putSingle(CACHE_POLICY, CacheableStorageProviderModel.CachePolicy.EVICT_DAILY.name()); + propProviderRW.getConfig().putSingle(EVICTION_HOUR, Integer.toString(eviction.get(HOUR_OF_DAY))); + propProviderRW.getConfig().putSingle(EVICTION_MINUTE, Integer.toString(eviction.get(MINUTE))); + testRealmResource().components().component(propProviderRWId).update(propProviderRW); + + // now + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + UserModel user = session.users().getUserByUsername("thor", realm); + }); + + // run twice to make sure its in cache. + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + UserModel user = session.users().getUserByUsername("thor", realm); + System.out.println("User class: " + user.getClass()); + Assert.assertTrue(user instanceof CachedUserModel); // should still be cached + }); + + setTimeOffset(2 * 60 * 60); // 2 hours in future + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + UserModel user = session.users().getUserByUsername("thor", realm); + System.out.println("User class: " + user.getClass()); + Assert.assertFalse(user instanceof CachedUserModel); // should be evicted + }); + + } + + + */ + + + @Test + public void testDailyEviction() { + testIsCached(); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + Calendar eviction = Calendar.getInstance(); + eviction.add(Calendar.HOUR, 1); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.EVICT_DAILY); + model.setEvictionHour(eviction.get(HOUR_OF_DAY)); + model.setEvictionMinute(eviction.get(MINUTE)); + realm.updateComponent(model); + }); + testIsCached(); + setTimeOffset(2 * 60 * 60); // 2 hours in future + testNotCached(); + testIsCached(); + + setDefaultCachePolicy(); + testIsCached(); + + } + @Test + public void testWeeklyEviction() { + testIsCached(); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + Calendar eviction = Calendar.getInstance(); + eviction.add(Calendar.HOUR, 4 * 24); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY); + model.setEvictionDay(eviction.get(DAY_OF_WEEK)); + model.setEvictionHour(eviction.get(HOUR_OF_DAY)); + model.setEvictionMinute(eviction.get(MINUTE)); + realm.updateComponent(model); + }); + testIsCached(); + setTimeOffset(2 * 24 * 60 * 60); // 2 days in future + testIsCached(); + setTimeOffset(5 * 24 * 60 * 60); // 5 days in future + testNotCached(); + testIsCached(); + + setDefaultCachePolicy(); + testIsCached(); + + } + @Test + public void testMaxLifespan() { + testIsCached(); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN); + model.setMaxLifespan(1 * 60 * 60 * 1000); + realm.updateComponent(model); + }); + testIsCached(); + + setTimeOffset(1/2 * 60 * 60); // 1/2 hour in future + + testIsCached(); + + setTimeOffset(2 * 60 * 60); // 2 hours in future + + testNotCached(); + testIsCached(); + + setDefaultCachePolicy(); + testIsCached(); + + } + + private void testNotCached() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientModel hardcoded = realm.getClientByClientId("hardcoded-client"); + Assert.assertNotNull(hardcoded); + Assert.assertFalse(hardcoded instanceof ClientAdapter); + }); + } + + + @Test + public void testIsCached() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientModel hardcoded = realm.getClientByClientId("hardcoded-client"); + Assert.assertNotNull(hardcoded); + Assert.assertTrue(hardcoded instanceof org.keycloak.models.cache.infinispan.ClientAdapter); + }); + } + + + @Test + public void testNoCache() { + testIsCached(); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.NO_CACHE); + realm.updateComponent(model); + }); + + testNotCached(); + + // test twice because updating component should evict + testNotCached(); + + // set it back + setDefaultCachePolicy(); + testIsCached(); + + + } + + private void setDefaultCachePolicy() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.DEFAULT); + realm.updateComponent(model); + }); + } }