This commit is contained in:
Bill Burke 2018-01-29 12:28:17 -05:00
parent 1d8e38f0c6
commit 4bf23cc83a
11 changed files with 445 additions and 233 deletions

View file

@ -39,14 +39,12 @@ import java.util.Set;
public class ClientAdapter implements ClientModel { public class ClientAdapter implements ClientModel {
protected RealmCacheSession cacheSession; protected RealmCacheSession cacheSession;
protected RealmModel cachedRealm; protected RealmModel cachedRealm;
protected RealmCache cache;
protected ClientModel updated; protected ClientModel updated;
protected CachedClient cached; 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.cachedRealm = cachedRealm;
this.cache = cache;
this.cacheSession = cacheSession; this.cacheSession = cacheSession;
this.cached = cached; this.cached = cached;
} }

View file

@ -24,6 +24,7 @@ import org.keycloak.models.*;
import org.keycloak.models.cache.CachedRealmModel; import org.keycloak.models.cache.CachedRealmModel;
import org.keycloak.models.cache.infinispan.entities.CachedRealm; import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.client.ClientStorageProvider;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -36,7 +37,6 @@ public class RealmAdapter implements CachedRealmModel {
protected CachedRealm cached; protected CachedRealm cached;
protected RealmCacheSession cacheSession; protected RealmCacheSession cacheSession;
protected volatile RealmModel updated; protected volatile RealmModel updated;
protected RealmCache cache;
protected KeycloakSession session; protected KeycloakSession session;
public RealmAdapter(KeycloakSession session, CachedRealm cached, RealmCacheSession cacheSession) { public RealmAdapter(KeycloakSession session, CachedRealm cached, RealmCacheSession cacheSession) {
@ -1323,35 +1323,37 @@ public class RealmAdapter implements CachedRealmModel {
@Override @Override
public ComponentModel addComponentModel(ComponentModel model) { public ComponentModel addComponentModel(ComponentModel model) {
getDelegateForUpdate(); getDelegateForUpdate();
evictUsers(model); executeEvictions(model);
return updated.addComponentModel(model); return updated.addComponentModel(model);
} }
@Override @Override
public ComponentModel importComponentModel(ComponentModel model) { public ComponentModel importComponentModel(ComponentModel model) {
getDelegateForUpdate(); getDelegateForUpdate();
evictUsers(model); executeEvictions(model);
return updated.importComponentModel(model); return updated.importComponentModel(model);
} }
public void evictUsers(ComponentModel model) { public void executeEvictions(ComponentModel model) {
String parentId = model.getParentId(); if (model == null) return;
evictUsers(parentId); // test that this is a realm component
} if (model.getParentId() != null && !model.getParentId().equals(getId())) return;
public void evictUsers(String parentId) { // invalidate entire user cache if we're dealing with user storage SPI
if (parentId != null && !parentId.equals(getId())) { if (UserStorageProvider.class.getName().equals(model.getProviderType())) {
ComponentModel parent = getComponent(parentId); session.userCache().evict(this);
if (parent != null && UserStorageProvider.class.getName().equals(parent.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 @Override
public void updateComponent(ComponentModel component) { public void updateComponent(ComponentModel component) {
getDelegateForUpdate(); getDelegateForUpdate();
evictUsers(component); executeEvictions(component);
updated.updateComponent(component); updated.updateComponent(component);
} }
@ -1359,7 +1361,7 @@ public class RealmAdapter implements CachedRealmModel {
@Override @Override
public void removeComponent(ComponentModel component) { public void removeComponent(ComponentModel component) {
getDelegateForUpdate(); getDelegateForUpdate();
evictUsers(component); executeEvictions(component);
updated.removeComponent(component); updated.removeComponent(component);
} }
@ -1367,7 +1369,6 @@ public class RealmAdapter implements CachedRealmModel {
@Override @Override
public void removeComponents(String parentId) { public void removeComponents(String parentId) {
getDelegateForUpdate(); getDelegateForUpdate();
evictUsers(parentId);
updated.removeComponents(parentId); updated.removeComponents(parentId);
} }

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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);
}

View file

@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ClusterProvider;
import org.keycloak.component.ComponentModel;
import org.keycloak.migration.MigrationModel; import org.keycloak.migration.MigrationModel;
import org.keycloak.models.*; import org.keycloak.models.*;
import org.keycloak.models.cache.CacheRealmProvider; 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.entities.*;
import org.keycloak.models.cache.infinispan.events.*; import org.keycloak.models.cache.infinispan.events.*;
import org.keycloak.models.utils.KeycloakModelUtils; 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.*; import java.util.*;
@ -100,7 +104,7 @@ public class RealmCacheSession implements CacheRealmProvider {
protected boolean setRollbackOnly; protected boolean setRollbackOnly;
protected Map<String, RealmAdapter> managedRealms = new HashMap<>(); protected Map<String, RealmAdapter> managedRealms = new HashMap<>();
protected Map<String, ClientAdapter> managedApplications = new HashMap<>(); protected Map<String, ClientModel> managedApplications = new HashMap<>();
protected Map<String, ClientTemplateAdapter> managedClientTemplates = new HashMap<>(); protected Map<String, ClientTemplateAdapter> managedClientTemplates = new HashMap<>();
protected Map<String, RoleAdapter> managedRoles = new HashMap<>(); protected Map<String, RoleAdapter> managedRoles = new HashMap<>();
protected Map<String, GroupAdapter> managedGroups = new HashMap<>(); protected Map<String, GroupAdapter> managedGroups = new HashMap<>();
@ -173,8 +177,8 @@ public class RealmCacheSession implements CacheRealmProvider {
private void invalidateClient(String id) { private void invalidateClient(String id) {
invalidations.add(id); invalidations.add(id);
ClientAdapter adapter = managedApplications.get(id); ClientModel adapter = managedApplications.get(id);
if (adapter != null) adapter.invalidate(); if (adapter != null && adapter instanceof ClientAdapter) ((ClientAdapter)adapter).invalidate();
} }
@Override @Override
@ -204,9 +208,9 @@ public class RealmCacheSession implements CacheRealmProvider {
invalidations.addAll(newInvalidations); invalidations.addAll(newInvalidations);
// need to make sure that scope and group mapping clients and groups are invalidated // need to make sure that scope and group mapping clients and groups are invalidated
for (String id : newInvalidations) { for (String id : newInvalidations) {
ClientAdapter adapter = managedApplications.get(id); ClientModel adapter = managedApplications.get(id);
if (adapter != null) { if (adapter != null && adapter instanceof ClientAdapter){
adapter.invalidate(); ((ClientAdapter)adapter).invalidate();
continue; continue;
} }
GroupAdapter group = managedGroups.get(id); GroupAdapter group = managedGroups.get(id);
@ -329,7 +333,6 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override @Override
public void commit() { public void commit() {
try { try {
if (realmDelegate == null) return;
if (clearAll) { if (clearAll) {
cache.clear(); cache.clear();
} }
@ -470,12 +473,16 @@ public class RealmCacheSession implements CacheRealmProvider {
RealmModel realm = getRealm(id); RealmModel realm = getRealm(id);
if (realm == null) return false; if (realm == null) return false;
cache.invalidateObject(id); evictRealmOnRemoval(realm);
invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName()));
cache.realmRemoval(id, realm.getName(), invalidations);
return getRealmDelegate().removeRealm(id); 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 @Override
public ClientModel addClient(RealmModel realm, String clientId) { public ClientModel addClient(RealmModel realm, String clientId) {
@ -1020,20 +1027,78 @@ public class RealmCacheSession implements CacheRealmProvider {
Long loaded = cache.getCurrentRevision(id); Long loaded = cache.getCurrentRevision(id);
ClientModel model = getClientDelegate().getClientById(id, realm); ClientModel model = getClientDelegate().getClientById(id, realm);
if (model == null) return null; if (model == null) return null;
if (invalidations.contains(id)) return model; ClientModel adapter = cacheClient(realm, model, loaded);
cached = new CachedClient(loaded, realm, model); managedApplications.put(id, adapter);
logger.tracev("adding client by id cache miss: {0}", cached.getClientId()); return adapter;
cache.addRevisioned(cached, startupRevision);
} else if (invalidations.contains(id)) { } else if (invalidations.contains(id)) {
return getRealmDelegate().getClientById(id, realm); return getRealmDelegate().getClientById(id, realm);
} else if (managedApplications.containsKey(id)) { } else if (managedApplications.containsKey(id)) {
return managedApplications.get(id); return managedApplications.get(id);
} }
ClientAdapter adapter = new ClientAdapter(realm, cached, this, null); ClientModel adapter = validateCache(realm, cached);
managedApplications.put(id, adapter); managedApplications.put(id, adapter);
return 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 @Override
public ClientModel getClientByClientId(String clientId, RealmModel realm) { public ClientModel getClientByClientId(String clientId, RealmModel realm) {
String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId()); String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());

View file

@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ClusterProvider;
import org.keycloak.models.cache.CachedObject;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Time; 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.UserFullInvalidationEvent;
import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent; import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent;
import org.keycloak.models.utils.ReadOnlyUserModelDelegate; import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
import org.keycloak.storage.CacheableStorageProviderModel;
import org.keycloak.storage.StorageId; import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.UserStorageProviderModel;
@ -144,7 +146,6 @@ public class UserCacheSession implements UserCache {
@Override @Override
public void commit() { public void commit() {
if (delegate == null) return;
runInvalidations(); runInvalidations();
transactionActive = false; transactionActive = false;
} }
@ -296,46 +297,11 @@ public class UserCacheSession implements UserCache {
if (!storageId.isLocal()) { if (!storageId.isLocal()) {
ComponentModel component = realm.getComponent(storageId.getProviderId()); 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 // although we do set a timeout, Infinispan has no guarantees when the user will be evicted
// its also hard to test stuff // its also hard to test stuff
boolean invalidate = false; if (model.shouldInvalidate(cached)) {
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) {
registerUserInvalidation(realm, cached); registerUserInvalidation(realm, cached);
return getDelegate().getUserById(cached.getId(), realm); return getDelegate().getUserById(cached.getId(), realm);
} }
@ -371,26 +337,11 @@ public class UserCacheSession implements UserCache {
adapter = new UserAdapter(cached, this, session, realm); adapter = new UserAdapter(cached, this, session, realm);
onCache(realm, adapter, delegate); onCache(realm, adapter, delegate);
if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) { long lifespan = model.getLifespan();
cache.addRevisioned(cached, startupRevision); if (lifespan > 0) {
cache.addRevisioned(cached, startupRevision, lifespan);
} else { } else {
long lifespan = -1; cache.addRevisioned(cached, startupRevision);
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 { } else {
cached = new CachedUser(revision, realm, delegate, notBefore); cached = new CachedUser(revision, realm, delegate, notBefore);
@ -402,39 +353,6 @@ public class UserCacheSession implements UserCache {
return adapter; 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) { private void onCache(RealmModel realm, UserAdapter adapter, UserModel delegate) {
((OnUserCache)getDelegate()).onCache(realm, adapter, delegate); ((OnUserCache)getDelegate()).onCache(realm, adapter, delegate);
((OnUserCache)session.userCredentialManager()).onCache(realm, adapter, delegate); ((OnUserCache)session.userCredentialManager()).onCache(realm, adapter, delegate);

View file

@ -1,6 +1,7 @@
package org.keycloak.models.cache.infinispan.entities; package org.keycloak.models.cache.infinispan.entities;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.cache.CachedObject;
import java.io.Serializable; import java.io.Serializable;
@ -8,7 +9,7 @@ import java.io.Serializable;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class AbstractRevisioned implements Revisioned, Serializable { public class AbstractRevisioned implements Revisioned, Serializable, CachedObject {
private String id; private String id;
private Long revision; private Long revision;
private final long cacheTimestamp = Time.currentTimeMillis(); private final long cacheTimestamp = Time.currentTimeMillis();
@ -38,6 +39,7 @@ public class AbstractRevisioned implements Revisioned, Serializable {
* *
* @return * @return
*/ */
@Override
public long getCacheTimestamp() { public long getCacheTimestamp() {
return cacheTimestamp; return cacheTimestamp;
} }

View file

@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan.initializer;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.models.cache.infinispan.UserCacheSession; import org.keycloak.models.cache.infinispan.UserCacheSession;
import org.keycloak.storage.CacheableStorageProviderModel;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.Calendar; import java.util.Calendar;
@ -65,13 +66,13 @@ public class InitializerStateTest {
@Test @Test
public void testDailyTimeout() throws Exception { 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)); 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)); 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)); 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(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
System.out.println("----"); System.out.println("----");
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
@ -80,7 +81,7 @@ public class InitializerStateTest {
int min = cal.get(Calendar.MINUTE); int min = cal.get(Calendar.MINUTE);
date = new Date(cal.getTimeInMillis()); date = new Date(cal.getTimeInMillis());
System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); 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)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
cal = Calendar.getInstance(); cal = Calendar.getInstance();
cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface CachedObject {
long getCacheTimestamp();
}

View file

@ -16,8 +16,12 @@
*/ */
package org.keycloak.storage; package org.keycloak.storage;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.component.PrioritizedComponentModel; import org.keycloak.component.PrioritizedComponentModel;
import org.keycloak.models.cache.CachedObject;
import java.util.Calendar;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -30,6 +34,7 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel {
public static final String EVICTION_MINUTE = "evictionMinute"; public static final String EVICTION_MINUTE = "evictionMinute";
public static final String EVICTION_DAY = "evictionDay"; public static final String EVICTION_DAY = "evictionDay";
public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore"; public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore";
public static final String ENABLED = "enabled";
private transient CachePolicy cachePolicy; private transient CachePolicy cachePolicy;
private transient long maxLifespan = -1; private transient long maxLifespan = -1;
@ -37,6 +42,7 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel {
private transient int evictionMinute = -1; private transient int evictionMinute = -1;
private transient int evictionDay = -1; private transient int evictionDay = -1;
private transient long cacheInvalidBefore = -1; private transient long cacheInvalidBefore = -1;
private transient Boolean enabled;
public CacheableStorageProviderModel() { public CacheableStorageProviderModel() {
} }
@ -137,6 +143,117 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel {
getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore)); 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 { public enum CachePolicy {
NO_CACHE, NO_CACHE,
DEFAULT, DEFAULT,

View file

@ -18,7 +18,6 @@
package org.keycloak.storage; package org.keycloak.storage;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.component.PrioritizedComponentModel;
/** /**
* Stored configuration of a User Storage provider instance. * 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 FULL_SYNC_PERIOD = "fullSyncPeriod";
public static final String CHANGED_SYNC_PERIOD = "changedSyncPeriod"; public static final String CHANGED_SYNC_PERIOD = "changedSyncPeriod";
public static final String LAST_SYNC = "lastSync"; public static final String LAST_SYNC = "lastSync";
public static final String ENABLED = "enabled";
public UserStorageProviderModel() { public UserStorageProviderModel() {
setProviderType(UserStorageProvider.class.getName()); setProviderType(UserStorageProvider.class.getName());
@ -46,7 +44,6 @@ public class UserStorageProviderModel extends CacheableStorageProviderModel {
private transient Integer changedSyncPeriod; private transient Integer changedSyncPeriod;
private transient Integer lastSync; private transient Integer lastSync;
private transient Boolean importEnabled; private transient Boolean importEnabled;
private transient Boolean enabled;
public boolean isImportEnabled() { public boolean isImportEnabled() {
if (importEnabled == null) { if (importEnabled == null) {
@ -66,24 +63,6 @@ public class UserStorageProviderModel extends CacheableStorageProviderModel {
getConfig().putSingle(IMPORT_ENABLED, Boolean.toString(flag)); 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() { public int getFullSyncPeriod() {
if (fullSyncPeriod == null) { if (fullSyncPeriod == null) {

View file

@ -30,17 +30,21 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowBindings; import org.keycloak.models.AuthenticationFlowBindings;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.infinispan.ClientAdapter;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.storage.CacheableStorageProviderModel;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.client.ClientStorageProvider;
import org.keycloak.storage.client.ClientStorageProviderModel;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
@ -66,9 +70,18 @@ import javax.ws.rs.core.Response;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Calendar;
import java.util.List; 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.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 * Test that clients can override auth flows
@ -92,6 +105,8 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest {
public void configureTestRealm(RealmRepresentation testRealm) { public void configureTestRealm(RealmRepresentation testRealm) {
} }
protected String providerId;
@Deployment @Deployment
public static WebArchive deploy() { public static WebArchive deploy() {
return RunOnServerDeployment.create(UserResource.class) 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.CLIENT_ID, "hardcoded-client");
provider.getConfig().putSingle(HardcodedClientStorageProviderFactory.REDIRECT_URI, oauth.getRedirectUri()); 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(); httpClient.close();
events.clear(); 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);
});
}
} }