Merge pull request #3426 from patriot1burke/master

user spi cache policy
This commit is contained in:
Bill Burke 2016-10-25 20:39:52 -04:00 committed by GitHub
commit 2578df1a82
16 changed files with 710 additions and 31 deletions

View file

@ -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();
}

View file

@ -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 {

View file

@ -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;

View file

@ -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));
}
}

View file

@ -20,8 +20,12 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -41,4 +45,15 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> ex
}
/**
* These are config properties that are common across all implementation of this component type
*
* @return
*/
default
List<ProviderConfigProperty> getCommonProviderConfigProperties() {
return Collections.EMPTY_LIST;
}
}

View file

@ -53,11 +53,17 @@ public class ComponentUtil {
private static Map<String, ProviderConfigProperty> getComponentConfigProperties(KeycloakSession session, String providerType, String providerId) {
try {
List<ProviderConfigProperty> l = getComponentFactory(session, providerType, providerId).getConfigProperties();
ComponentFactory componentFactory = getComponentFactory(session, providerType, providerId);
List<ProviderConfigProperty> l = componentFactory.getConfigProperties();
Map<String, ProviderConfigProperty> properties = new HashMap<>();
for (ProviderConfigProperty p : l) {
properties.put(p.getName(), p);
}
List<ProviderConfigProperty> common = componentFactory.getCommonProviderConfigProperties();
for (ProviderConfigProperty p : common) {
properties.put(p.getName(), p);
}
return properties;
} catch (Exception e) {
throw new RuntimeException(e);

View file

@ -626,7 +626,9 @@ public class ModelToRepresentation {
providerRep.setStoreToken(identityProviderModel.isStoreToken());
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
providerRep.setAuthenticateByDefault(identityProviderModel.isAuthenticateByDefault());
providerRep.setConfig(new HashMap<>(identityProviderModel.getConfig()));
Map<String, String> config = new HashMap<>();
config.putAll(identityProviderModel.getConfig());
providerRep.setConfig(config);
providerRep.setAddReadTokenRoleOnCreate(identityProviderModel.isAddReadTokenRoleOnCreate());
String firstBrokerLoginFlowId = identityProviderModel.getFirstBrokerLoginFlowId();

View file

@ -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;

View file

@ -25,8 +25,10 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
@ -95,4 +97,15 @@ public interface UserStorageProviderFactory<T extends UserStorageProvider> exten
default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
/**
* configuration properties that are common across all UserStorageProvider implementations
*
* @return
*/
@Override
default
List<ProviderConfigProperty> getCommonProviderConfigProperties() {
return UserStorageProviderSpi.commonConfig();
}
}

View file

@ -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));

View file

@ -18,9 +18,14 @@
package org.keycloak.storage;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -46,4 +51,39 @@ public class UserStorageProviderSpi implements Spi {
return UserStorageProviderFactory.class;
}
private static final List<ProviderConfigProperty> commonConfig;
static {
List<ProviderConfigProperty> config = ProviderConfigurationBuilder.create()
.property()
.name("priority").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("fullSyncPeriod").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("changedSyncPeriod").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("lastSync").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("importEnabled").type(ProviderConfigProperty.BOOLEAN_TYPE).add()
.property()
.name("cachePolicy").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("maxLifespan").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionHour").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionMinute").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionDay").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("cacheInvalidBefore").type(ProviderConfigProperty.STRING_TYPE).add()
.build();
commonConfig = Collections.unmodifiableList(config);
}
public static List<ProviderConfigProperty> commonConfig() {
return commonConfig;
}
}

View file

@ -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;
@ -89,6 +90,7 @@ public class ComponentResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<ComponentRepresentation> getComponents(@QueryParam("parent") String parent, @QueryParam("type") String type) {
auth.requireView();
List<ComponentModel> components = Collections.EMPTY_LIST;
@ -130,13 +132,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

View file

@ -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();

View file

@ -1179,3 +1179,30 @@ add-keystore=Add Keystore
add-keystore.placeholder=Add keystore...
view=View
active=Active
Sunday=Sunday
Monday=Monday
Tuesday=Tuesday
Wednesday=Wednesday
Thursday=Thursday
Friday=Friday
Saturday=Saturday
user-storage-cache-policy=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.

View file

@ -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);

View file

@ -68,6 +68,154 @@
</fieldset>
<fieldset>
<legend><span class="text">{{:: 'user-storage-cache-policy' | translate}}</span></legend>
<div class="form-group">
<label for="cachePolicy" class="col-md-2 control-label">{{:: 'userStorage.cachePolicy' | translate}}</label>
<div class="col-md-2">
<div>
<select id="cachePolicy" ng-model="instance.config['cachePolicy'][0]" class="form-control">
<option value="DEFAULT">{{:: 'userStorage.cachePolicy.option.DEFAULT' | translate}}</option>
<option value="EVICT_DAILY">{{:: 'userStorage.cachePolicy.option.EVICT_DAILY' | translate}}</option>
<option value="EVICT_WEEKLY">{{:: 'userStorage.cachePolicy.option.EVICT_WEEKLY' | translate}}</option>
<option value="MAX_LIFESPAN">{{:: 'userStorage.cachePolicy.option.MAX_LIFESPAN' | translate}}</option>
<option value="NO_CACHE">{{:: 'userStorage.cachePolicy.option.NO_CACHE' | translate}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'userStorage.cachePolicy.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY'">
<label for="evictionDay" class="col-md-2 control-label">{{:: 'userStorage.evictionDay' | translate}}</label>
<div class="col-md-2">
<div>
<select id="evictionDay" ng-model="instance.config['evictionDay'][0]" class="form-control">
<option value="1">{{:: 'Sunday' | translate}}</option>
<option value="2">{{:: 'Monday' | translate}}</option>
<option value="3">{{:: 'Tuesday' | translate}}</option>
<option value="4">{{:: 'Wednesday' | translate}}</option>
<option value="5">{{:: 'Thursday' | translate}}</option>
<option value="6">{{:: 'Friday' | translate}}</option>
<option value="7">{{:: 'Saturday' | translate}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'userStorage.cachePolicy.evictionDay.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY' || instance.config['cachePolicy'][0] == 'EVICT_DAILY'">
<label class="col-md-2 control-label" for="evictionHour">{{:: 'userStorage.cachePolicy.evictionHour' | translate}}</label>
<div class="col-md-2">
<div>
<select id="evictionHour" ng-model="instance.config['evictionHour'][0]" class="form-control">
<option value="0">00</option>
<option value="1">01</option>
<option value="2">02</option>
<option value="3">03</option>
<option value="4">04</option>
<option value="5">05</option>
<option value="6">06</option>
<option value="7">07</option>
<option value="8">08</option>
<option value="9">09</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
<option value="15">15</option>
<option value="16">16</option>
<option value="17">17</option>
<option value="18">18</option>
<option value="19">19</option>
<option value="20">20</option>
<option value="21">21</option>
<option value="22">22</option>
<option value="23">23</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'userStorage.cachePolicy.evictionHour.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY' || instance.config['cachePolicy'][0] == 'EVICT_DAILY'">
<label class="col-md-2 control-label" for="evictionMinute">{{:: 'userStorage.cachePolicy.evictionMinute' | translate}}</label>
<div class="col-md-2">
<div>
<select id="evictionMinute" ng-model="instance.config['evictionMinute'][0]" class="form-control">
<option value="0">00</option>
<option value="1">01</option>
<option value="2">02</option>
<option value="3">03</option>
<option value="4">04</option>
<option value="5">05</option>
<option value="6">06</option>
<option value="7">07</option>
<option value="8">08</option>
<option value="9">09</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
<option value="15">15</option>
<option value="16">16</option>
<option value="17">17</option>
<option value="18">18</option>
<option value="19">19</option>
<option value="20">20</option>
<option value="21">21</option>
<option value="22">22</option>
<option value="23">23</option>
<option value="24">24</option>
<option value="25">25</option>
<option value="26">26</option>
<option value="27">27</option>
<option value="28">28</option>
<option value="29">29</option>
<option value="30">30</option>
<option value="31">31</option>
<option value="32">32</option>
<option value="33">33</option>
<option value="34">34</option>
<option value="35">35</option>
<option value="36">36</option>
<option value="37">37</option>
<option value="38">38</option>
<option value="39">39</option>
<option value="40">40</option>
<option value="41">41</option>
<option value="42">42</option>
<option value="43">43</option>
<option value="44">44</option>
<option value="45">45</option>
<option value="46">46</option>
<option value="47">47</option>
<option value="48">48</option>
<option value="49">49</option>
<option value="50">50</option>
<option value="51">51</option>
<option value="52">52</option>
<option value="53">53</option>
<option value="54">54</option>
<option value="55">55</option>
<option value="56">56</option>
<option value="57">57</option>
<option value="58">58</option>
<option value="59">59</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'userStorage.cachePolicy.evictionMinute.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'MAX_LIFESPAN'">
<label class="col-md-2 control-label" for="maxLifespan">{{:: 'userStorage.cachePolicy.maxLifespan' | translate}}</label>
<div class="col-md-6">
<input class="form-control" type="text" ng-model="instance.config['maxLifespan'][0]" id="maxLifespan" />
</div>
<kc-tooltip>{{:: 'userStorage.cachePolicy.maxLifespan.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageUsers">
<button kc-save>{{:: 'save' | translate}}</button>