Merge pull request #3511 from mposolda/ispn-invalidations-rebase

KEYCLOAK-3857 Clustered invalidation cache fixes and refactoring. Sup…
This commit is contained in:
Marek Posolda 2016-11-16 23:10:48 +01:00 committed by GitHub
commit 5e496fb9bb
75 changed files with 3271 additions and 768 deletions

View file

@ -30,6 +30,9 @@
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.infinispan"/>
<module name="org.infinispan.commons"/>
<module name="org.infinispan.cachestore.remote"/>
<module name="org.infinispan.client.hotrod"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>

View file

@ -2,9 +2,9 @@ embed-server --server-config=standalone-ha.xml
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
/subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000)
/subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC")
/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users:add(mode="SYNC")
/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")

116
misc/CrossDataCenter.md Normal file
View file

@ -0,0 +1,116 @@
Test Cross-Data-Center scenario (test with external JDG server)
===============================================================
These are temporary notes. This docs should be removed once we have cross-DC support finished and properly documented.
What is working right now is:
- Propagating of invalidation messages for "realms" and "users" caches
- All the other things provided by ClusterProvider, which is:
-- ClusterStartupTime (used for offlineSessions and revokeRefreshToken) is shared for all clusters in all datacenters
-- Periodic userStorage synchronization is always executed just on one node at a time. It won't be never executed concurrently on more nodes (Assuming "nodes" refer to all servers in all clusters in all datacenters)
What doesn't work right now:
- UserSessionProvider and offline sessions
Basic setup
===========
This is setup with 2 keycloak nodes, which are NOT in cluster. They just share the same database and they will be configured with "work" infinispan cache with remoteStore, which will point
to external JDG server.
JDG Server setup
----------------
- Download JDG 7.0 server and unzip to some folder
- Add this into JDG_HOME/standalone/configuration/standalone.xml under cache-container named "local" :
```
<local-cache name="work" start="EAGER" batching="false" />
```
- Start server:
```
cd JDG_HOME/bin
./standalone.sh -Djboss.socket.binding.port-offset=100
```
Keycloak servers setup
----------------------
You need to setup 2 Keycloak nodes in this way.
For now, it's recommended to test Keycloak overlay on EAP7 because of infinispan bug, which is fixed in EAP 7.0 (infinispan 8.1.2), but not
yet on Wildfly 10 (infinispan 8.1.0). See below for details.
1) Configure shared database in KEYCLOAK_HOME/standalone/configuration/standalone.xml . For example MySQL
2) Add `module` attribute to the infinispan keycloak container:
```
<cache-container name="keycloak" jndi-name="infinispan/Keycloak" module="org.keycloak.keycloak-model-infinispan">
```
3) Configure `work` cache to use remoteStore. You should use this:
```
<local-cache name="work">
<remote-store passivation="false" fetch-state="false" purge="false" preload="false" shared="true" cache="work" remote-servers="remote-cache">
<property name="rawValues">true</property>
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
</remote-store>
</local-cache>
```
4) Configure connection to the external JDG server. Because we used port offset 100 for JDG (see above), the HotRod endpoint is running on 11322 .
So add the config like this to the bottom of standalone.xml under `socket-binding-group` element:
```
<outbound-socket-binding name="remote-cache">
<remote-destination host="localhost" port="11322"/>
</outbound-socket-binding>
```
5) Optional: Configure logging in standalone.xml to see what invalidation events were send:
````
<logger category="org.keycloak.cluster.infinispan">
<level name="TRACE"/>
</logger>
<logger category="org.keycloak.models.cache.infinispan">
<level name="DEBUG"/>
</logger>
````
6) Setup Keycloak node2 . Just copy Keycloak to another location on your laptop and repeat steps 1-5 above for second server too.
7) Run server 1 with parameters like (assuming you have virtual hosts "node1" and "node2" defined in your `/etc/hosts` ):
```
./standalone.sh -Djboss.node.name=node1 -b node1 -bmanagement node1
```
and server2 with:
```
./standalone.sh -Djboss.node.name=node2 -b node2 -bmanagement node2
```
8) Note something like this in both `KEYCLOAK_HOME/standalone/log/server.log` on both nodes. Note that cluster Startup Time will be same time on both nodes:
```
2016-11-16 22:12:52,080 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) My address: node1-1953169551
2016-11-16 22:12:52,081 DEBUG [org.keycloak.cluster.infinispan.CrossDCAwareCacheFactory] (ServerService Thread Pool -- 62) RemoteStore is available. Cross-DC scenario will be used
2016-11-16 22:12:52,119 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) Loaded cluster startup time: Wed Nov 16 22:09:48 CET 2016
2016-11-16 22:12:52,128 DEBUG [org.keycloak.cluster.infinispan.InfinispanNotificationsManager] (ServerService Thread Pool -- 62) Added listener for HotRod remoteStore cache: work
```
9) Login to node1. Then change any realm on node2. You will see in the node2 server.log that RealmUpdatedEvent was sent and on node1 that this event was received.
This is done even if node1 and node2 are NOT in cluster as it's the external JDG used for communication between 2 keycloak servers and sending/receiving cache invalidation events. But note that userSession
doesn't yet work (eg. if you login to node1, you won't see the userSession on node2).
WARNING: Previous steps works on Keycloak server overlay deployed on EAP 7.0 . With deploy on Wildfly 10.0.0.Final, you will see exception
at startup caused by the bug https://issues.jboss.org/browse/ISPN-6203 .
There is a workaround to add this line into KEYCLOAK_HOME/modules/system/layers/base/org/wildfly/clustering/service/main/module.xml :
```
<module name="org.infinispan.client.hotrod"/>
```

View file

@ -48,6 +48,10 @@
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-cachestore-remote</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View file

@ -0,0 +1,93 @@
/*
* 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.cluster.infinispan;
import java.io.Serializable;
import java.util.Set;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.Flag;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.commons.api.BasicCache;
import org.infinispan.persistence.remote.RemoteStore;
import org.jboss.logging.Logger;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
abstract class CrossDCAwareCacheFactory {
protected static final Logger logger = Logger.getLogger(CrossDCAwareCacheFactory.class);
abstract BasicCache<String, Serializable> getCache();
static CrossDCAwareCacheFactory getFactory(Cache<String, Serializable> workCache, Set<RemoteStore> remoteStores) {
if (remoteStores.isEmpty()) {
logger.debugf("No configured remoteStore available. Cross-DC scenario is not used");
return new InfinispanCacheWrapperFactory(workCache);
} else {
logger.debugf("RemoteStore is available. Cross-DC scenario will be used");
if (remoteStores.size() > 1) {
logger.warnf("More remoteStores configured for work cache. Will use just the first one");
}
// For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly
RemoteStore remoteStore = remoteStores.iterator().next();
RemoteCache remoteCache = remoteStore.getRemoteCache();
return new RemoteCacheWrapperFactory(remoteCache);
}
}
// We don't have external JDG configured. No cross-DC.
private static class InfinispanCacheWrapperFactory extends CrossDCAwareCacheFactory {
private final Cache<String, Serializable> workCache;
InfinispanCacheWrapperFactory(Cache<String, Serializable> workCache) {
this.workCache = workCache;
}
@Override
BasicCache<String, Serializable> getCache() {
return workCache;
}
}
// We have external JDG configured. Cross-DC should be enabled
private static class RemoteCacheWrapperFactory extends CrossDCAwareCacheFactory {
private final RemoteCache<String, Serializable> remoteCache;
RemoteCacheWrapperFactory(RemoteCache<String, Serializable> remoteCache) {
this.remoteCache = remoteCache;
}
@Override
BasicCache<String, Serializable> getCache() {
// Flags are per-invocation!
return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE);
}
}
}

View file

@ -17,20 +17,15 @@
package org.keycloak.cluster.infinispan;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.cluster.ExecutionResult;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import java.io.Serializable;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
*
@ -43,34 +38,22 @@ public class InfinispanClusterProvider implements ClusterProvider {
public static final String CLUSTER_STARTUP_TIME_KEY = "cluster-start-time";
private static final String TASK_KEY_PREFIX = "task::";
private final InfinispanClusterProviderFactory factory;
private final KeycloakSession session;
private final Cache<String, Serializable> cache;
private final int clusterStartupTime;
private final String myAddress;
private final CrossDCAwareCacheFactory crossDCAwareCacheFactory;
private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class
public InfinispanClusterProvider(InfinispanClusterProviderFactory factory, KeycloakSession session, Cache<String, Serializable> cache) {
this.factory = factory;
this.session = session;
this.cache = cache;
public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager) {
this.myAddress = myAddress;
this.clusterStartupTime = clusterStartupTime;
this.crossDCAwareCacheFactory = crossDCAwareCacheFactory;
this.notificationsManager = notificationsManager;
}
@Override
public int getClusterStartupTime() {
Integer existingClusterStartTime = (Integer) cache.get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
if (existingClusterStartTime != null) {
return existingClusterStartTime;
} else {
// clusterStartTime not yet initialized. Let's try to put our startupTime
int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
existingClusterStartTime = (Integer) cache.putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
if (existingClusterStartTime == null) {
logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
return serverStartTime;
} else {
return existingClusterStartTime;
}
}
return clusterStartupTime;
}
@ -104,56 +87,33 @@ public class InfinispanClusterProvider implements ClusterProvider {
@Override
public void registerListener(String taskKey, ClusterListener task) {
factory.registerListener(taskKey, task);
this.notificationsManager.registerListener(taskKey, task);
}
@Override
public void notify(String taskKey, ClusterEvent event) {
// Put the value to the cache to notify listeners on all the nodes
cache.put(taskKey, event);
public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
this.notificationsManager.notify(taskKey, event, ignoreSender);
}
private String getCurrentNode(Cache<String, Serializable> cache) {
Transport transport = cache.getCacheManager().getTransport();
return transport==null ? "local" : transport.getAddress().toString();
}
private LockEntry createLockEntry(Cache<String, Serializable> cache) {
private LockEntry createLockEntry() {
LockEntry lock = new LockEntry();
lock.setNode(getCurrentNode(cache));
lock.setNode(myAddress);
lock.setTimestamp(Time.currentTime());
return lock;
}
private boolean tryLock(String cacheKey, int taskTimeoutInSeconds) {
LockEntry myLock = createLockEntry(cache);
LockEntry myLock = createLockEntry();
LockEntry existingLock = (LockEntry) cache.putIfAbsent(cacheKey, myLock);
LockEntry existingLock = (LockEntry) crossDCAwareCacheFactory.getCache().putIfAbsent(cacheKey, myLock, taskTimeoutInSeconds, TimeUnit.SECONDS);
if (existingLock != null) {
// Task likely already in progress. Check if timestamp is not outdated
int thatTime = existingLock.getTimestamp();
int currentTime = Time.currentTime();
if (thatTime + taskTimeoutInSeconds < currentTime) {
if (logger.isTraceEnabled()) {
logger.tracef("Task %s outdated when in progress by node %s. Will try to replace task with our node %s", cacheKey, existingLock.getNode(), myLock.getNode());
}
boolean replaced = cache.replace(cacheKey, existingLock, myLock);
if (!replaced) {
if (logger.isTraceEnabled()) {
logger.tracef("Failed to replace the task %s. Other thread replaced in the meantime. Ignoring task.", cacheKey);
}
}
return replaced;
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
}
return false;
if (logger.isTraceEnabled()) {
logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
}
return false;
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Successfully acquired lock for task %s. Our node is %s", cacheKey, myLock.getNode());
@ -168,20 +128,12 @@ public class InfinispanClusterProvider implements ClusterProvider {
int retry = 3;
while (true) {
try {
cache.getAdvancedCache()
.withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
.remove(cacheKey);
crossDCAwareCacheFactory.getCache().remove(cacheKey);
if (logger.isTraceEnabled()) {
logger.tracef("Task %s removed from the cache", cacheKey);
}
return;
} catch (RuntimeException e) {
ComponentStatus status = cache.getStatus();
if (status.isStopping() || status.isTerminated()) {
logger.warnf("Failed to remove task %s from the cache. Cache is already terminating", cacheKey);
logger.debug(e.getMessage(), e);
return;
}
retry--;
if (retry == 0) {
throw e;

View file

@ -20,27 +20,24 @@ package org.keycloak.cluster.infinispan;
import org.infinispan.Cache;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.Address;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.cluster.ClusterProviderFactory;
import org.keycloak.common.util.HostUtils;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
@ -49,6 +46,8 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* This impl is aware of Cross-Data-Center scenario too
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanClusterProviderFactory implements ClusterProviderFactory {
@ -57,28 +56,82 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
protected static final Logger logger = Logger.getLogger(InfinispanClusterProviderFactory.class);
// Infinispan cache
private volatile Cache<String, Serializable> workCache;
private Map<String, ClusterListener> listeners = new HashMap<>();
// Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups
private CrossDCAwareCacheFactory crossDCAwareCacheFactory;
private String myAddress;
private int clusterStartupTime;
// Just to extract notifications related stuff to separate class
private InfinispanNotificationsManager notificationsManager;
@Override
public ClusterProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanClusterProvider(this, session, workCache);
return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager);
}
private void lazyInit(KeycloakSession session) {
if (workCache == null) {
synchronized (this) {
if (workCache == null) {
workCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
InfinispanConnectionProvider ispnConnections = session.getProvider(InfinispanConnectionProvider.class);
workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
workCache.getCacheManager().addListener(new ViewChangeListener());
workCache.addListener(new CacheEntryListener());
initMyAddress();
Set<RemoteStore> remoteStores = getRemoteStores();
crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores);
clusterStartupTime = initClusterStartupTime(session);
notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, remoteStores);
}
}
}
}
// See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
private Set<RemoteStore> getRemoteStores() {
return workCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
}
protected void initMyAddress() {
Transport transport = workCache.getCacheManager().getTransport();
this.myAddress = transport == null ? HostUtils.getHostName() + "-" + workCache.hashCode() : transport.getAddress().toString();
logger.debugf("My address: %s", this.myAddress);
}
protected int initClusterStartupTime(KeycloakSession session) {
Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
if (existingClusterStartTime != null) {
logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
return existingClusterStartTime;
} else {
// clusterStartTime not yet initialized. Let's try to put our startupTime
int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
if (existingClusterStartTime == null) {
logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
return serverStartTime;
} else {
logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
return existingClusterStartTime;
}
}
}
@Override
public void init(Config.Scope config) {
}
@ -167,34 +220,4 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
}
<T> void registerListener(String taskKey, ClusterListener task) {
listeners.put(taskKey, task);
}
@Listener
public class CacheEntryListener {
@CacheEntryCreated
public void cacheEntryCreated(CacheEntryCreatedEvent<String, Object> event) {
if (!event.isPre()) {
trigger(event.getKey(), event.getValue());
}
}
@CacheEntryModified
public void cacheEntryModified(CacheEntryModifiedEvent<String, Object> event) {
if (!event.isPre()) {
trigger(event.getKey(), event.getValue());
}
}
private void trigger(String key, Object value) {
ClusterListener task = listeners.get(key);
if (task != null) {
ClusterEvent event = (ClusterEvent) value;
task.run(event);
}
}
}
}

View file

@ -0,0 +1,204 @@
/*
* 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.cluster.infinispan;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.client.hotrod.event.ClientEvent;
import org.infinispan.context.Flag;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.HostUtils;
import org.keycloak.common.util.MultivaluedHashMap;
/**
* Impl for sending infinispan messages across cluster and listening to them
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanNotificationsManager {
protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class);
private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>();
private final Cache<String, Serializable> workCache;
private final String myAddress;
protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, String myAddress) {
this.workCache = workCache;
this.myAddress = myAddress;
}
// Create and init manager including all listeners etc
public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, Set<RemoteStore> remoteStores) {
InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress);
// We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener
if (remoteStores.isEmpty()) {
workCache.addListener(manager.new CacheEntryListener());
logger.debugf("Added listener for infinispan cache: %s", workCache.getName());
} else {
for (RemoteStore remoteStore : remoteStores) {
RemoteCache<Object, Object> remoteCache = remoteStore.getRemoteCache();
remoteCache.addClientListener(manager.new HotRodListener(remoteCache));
logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName());
}
}
return manager;
}
void registerListener(String taskKey, ClusterListener task) {
listeners.add(taskKey, task);
}
void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
WrapperClusterEvent wrappedEvent = new WrapperClusterEvent();
wrappedEvent.setDelegateEvent(event);
wrappedEvent.setIgnoreSender(ignoreSender);
wrappedEvent.setSender(myAddress);
if (logger.isTraceEnabled()) {
logger.tracef("Sending event %s: %s", taskKey, event);
}
// Put the value to the cache to notify listeners on all the nodes
workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
.put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS);
}
@Listener(observation = Listener.Observation.POST)
public class CacheEntryListener {
@CacheEntryCreated
public void cacheEntryCreated(CacheEntryCreatedEvent<String, Serializable> event) {
eventReceived(event.getKey(), event.getValue());
}
@CacheEntryModified
public void cacheEntryModified(CacheEntryModifiedEvent<String, Serializable> event) {
eventReceived(event.getKey(), event.getValue());
}
}
@ClientListener
public class HotRodListener {
private final RemoteCache<Object, Object> remoteCache;
public HotRodListener(RemoteCache<Object, Object> remoteCache) {
this.remoteCache = remoteCache;
}
@ClientCacheEntryCreated
public void created(ClientCacheEntryCreatedEvent event) {
String key = event.getKey().toString();
hotrodEventReceived(key);
}
@ClientCacheEntryModified
public void updated(ClientCacheEntryModifiedEvent event) {
String key = event.getKey().toString();
hotrodEventReceived(key);
}
private void hotrodEventReceived(String key) {
// TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
Object value = remoteCache.get(key);
Serializable rawValue;
if (value instanceof MarshalledEntry) {
Object rw = ((MarshalledEntry)value).getValue();
rawValue = (Serializable) rw;
} else {
rawValue = (Serializable) value;
}
eventReceived(key, rawValue);
}
}
private void eventReceived(String key, Serializable obj) {
if (!(obj instanceof WrapperClusterEvent)) {
return;
}
WrapperClusterEvent event = (WrapperClusterEvent) obj;
if (event.isIgnoreSender()) {
if (this.myAddress.equals(event.getSender())) {
return;
}
}
if (logger.isTraceEnabled()) {
logger.tracef("Received event %s: %s", key, event);
}
ClusterEvent wrappedEvent = event.getDelegateEvent();
List<ClusterListener> myListeners = listeners.get(key);
if (myListeners != null) {
for (ClusterListener listener : myListeners) {
listener.eventReceived(wrappedEvent);
}
}
myListeners = listeners.get(ClusterProvider.ALL);
if (myListeners != null) {
for (ClusterListener listener : myListeners) {
listener.eventReceived(wrappedEvent);
}
}
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.cluster.infinispan;
import org.infinispan.commons.marshall.jboss.GenericJBossMarshaller;
/**
* Needed on Wildfly, so that remoteStore (hotRod client) can find our classes
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KeycloakHotRodMarshallerFactory {
public static GenericJBossMarshaller getInstance() {
return new GenericJBossMarshaller(KeycloakHotRodMarshallerFactory.class.getClassLoader());
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.cluster.infinispan;
import org.keycloak.cluster.ClusterEvent;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class WrapperClusterEvent implements ClusterEvent {
private String sender; // will be null in non-clustered environment
private boolean ignoreSender;
private ClusterEvent delegateEvent;
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public boolean isIgnoreSender() {
return ignoreSender;
}
public void setIgnoreSender(boolean ignoreSender) {
this.ignoreSender = ignoreSender;
}
public ClusterEvent getDelegateEvent() {
return delegateEvent;
}
public void setDelegateEvent(ClusterEvent delegateEvent) {
this.delegateEvent = delegateEvent;
}
@Override
public String toString() {
return String.format("WrapperClusterEvent [ sender=%s, delegateEvent=%s ]", sender, delegateEvent.toString());
}
}

View file

@ -27,11 +27,14 @@ import org.infinispan.eviction.EvictionStrategy;
import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.remote.configuration.ExhaustedAction;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -126,7 +129,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = config.getBoolean("clustered", false);
boolean async = config.getBoolean("async", true);
boolean async = config.getBoolean("async", false);
boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
if (clustered) {
@ -139,14 +142,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
logger.debug("Started embedded Infinispan cache container");
ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
if (clustered) {
invalidationConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
}
Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build();
ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder();
Configuration modelCacheConfiguration = modelCacheConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, invalidationCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, modelCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, modelCacheConfiguration);
ConfigurationBuilder sessionConfigBuilder = new ConfigurationBuilder();
if (clustered) {
@ -174,8 +174,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
if (clustered) {
replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
}
Configuration replicationCacheConfiguration = replicationConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationCacheConfiguration);
boolean jdgEnabled = config.getBoolean("remoteStoreEnabled");
if (jdgEnabled) {
configureRemoteCacheStore(replicationConfigBuilder, async);
}
Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationEvictionCacheConfiguration);
ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
counterConfigBuilder.invocationBatching().enable()
@ -211,6 +217,34 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
return cb.build();
}
// Used for cross-data centers scenario. Usually integration with external JDG server, which itself handles communication between DCs.
private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async) {
String jdgServer = config.get("remoteStoreServer", "localhost");
Integer jdgPort = config.getInt("remoteStorePort", 11222);
builder.persistence()
.passivation(false)
.addStore(RemoteStoreConfigurationBuilder.class)
.fetchPersistentState(false)
.ignoreModifications(false)
.purgeOnStartup(false)
.preload(false)
.shared(true)
.remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
.rawValues(true)
.forceReturnValues(false)
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
.addServer()
.host(jdgServer)
.port(jdgPort)
// .connectionPool()
// .maxActive(100)
// .exhaustedAction(ExhaustedAction.CREATE_NEW)
.async()
.enabled(async);
}
protected Configuration getKeysCacheConfig() {
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);

View file

@ -1,14 +1,13 @@
package org.keycloak.models.cache.infinispan;
import org.infinispan.Cache;
import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated;
import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
@ -55,7 +54,7 @@ import java.util.function.Predicate;
* @version $Revision: 1 $
*/
public abstract class CacheManager {
protected static final Logger logger = Logger.getLogger(CacheManager.class);
protected final Cache<String, Long> revisions;
protected final Cache<String, Revisioned> cache;
protected final UpdateCounter counter = new UpdateCounter();
@ -63,9 +62,10 @@ public abstract class CacheManager {
public CacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
this.cache = cache;
this.revisions = revisions;
this.cache.addListener(this);
}
protected abstract Logger getLogger();
public Cache<String, Revisioned> getCache() {
return cache;
}
@ -79,10 +79,7 @@ public abstract class CacheManager {
if (revision == null) {
revision = counter.current();
}
// if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
// so, we do this to force this.
String invalidationKey = "invalidation.key" + id;
cache.putForExternalRead(invalidationKey, new AbstractRevisioned(-1L, invalidationKey));
return revision;
}
@ -101,12 +98,16 @@ public abstract class CacheManager {
}
Long rev = revisions.get(id);
if (rev == null) {
RealmCacheManager.logger.tracev("get() missing rev");
if (getLogger().isTraceEnabled()) {
getLogger().tracev("get() missing rev {0}", id);
}
return null;
}
long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue();
if (rev > oRev) {
RealmCacheManager.logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
if (getLogger().isTraceEnabled()) {
getLogger().tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
}
return null;
}
return o != null && type.isInstance(o) ? type.cast(o) : null;
@ -114,9 +115,11 @@ public abstract class CacheManager {
public Object invalidateObject(String id) {
Revisioned removed = (Revisioned)cache.remove(id);
// if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
// so, we do this to force the event.
cache.remove("invalidation.key" + id);
if (getLogger().isTraceEnabled()) {
getLogger().tracef("Removed key='%s', value='%s' from cache", id, removed);
}
bumpVersion(id);
return removed;
}
@ -137,37 +140,35 @@ public abstract class CacheManager {
//revisions.getAdvancedCache().lock(id);
Long rev = revisions.get(id);
if (rev == null) {
if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev == null realm.clients");
rev = counter.current();
revisions.put(id, rev);
}
revisions.startBatch();
if (!revisions.getAdvancedCache().lock(id)) {
RealmCacheManager.logger.trace("Could not obtain version lock");
if (getLogger().isTraceEnabled()) {
getLogger().tracev("Could not obtain version lock: {0}", id);
}
return;
}
rev = revisions.get(id);
if (rev == null) {
if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev2 == null realm.clients");
return;
}
if (rev > startupRevision) { // revision is ahead transaction start. Other transaction updated in the meantime. Don't cache
if (RealmCacheManager.logger.isTraceEnabled()) {
RealmCacheManager.logger.tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
if (getLogger().isTraceEnabled()) {
getLogger().tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
}
return;
}
if (rev.equals(object.getRevision())) {
if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev);
cache.putForExternalRead(id, object);
return;
}
if (rev > object.getRevision()) { // revision is ahead, don't cache
if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned revision is ahead realm.clients");
if (getLogger().isTraceEnabled()) getLogger().tracev("Skipped cache. Object revision {0}, Cache revision {1}", object.getRevision(), rev);
return;
}
// 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());
if (lifespan < 0) cache.putForExternalRead(id, object);
else cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
@ -196,63 +197,36 @@ public abstract class CacheManager {
.filter(predicate).iterator();
}
@CacheEntryInvalidated
public void cacheInvalidated(CacheEntryInvalidatedEvent<String, Object> event) {
if (event.isPre()) {
String key = event.getKey();
if (key.startsWith("invalidation.key")) {
// if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
// so, we do this to force this.
String bump = key.substring("invalidation.key".length());
RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump);
bumpVersion(bump);
return;
}
} else {
//if (!event.isPre()) {
String key = event.getKey();
if (key.startsWith("invalidation.key")) {
// if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
// so, we do this to force this.
String bump = key.substring("invalidation.key".length());
bumpVersion(bump);
RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump);
return;
}
bumpVersion(key);
Object object = event.getValue();
if (object != null) {
bumpVersion(key);
Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
if (predicate != null) runEvictions(predicate);
RealmCacheManager.logger.tracev("invalidating: {0}" + object.getClass().getName());
}
public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents) {
ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class);
// Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more.
for (InvalidationEvent event : invalidationEvents) {
clusterProvider.notify(generateEventId(event), event, true);
}
}
@CacheEntriesEvicted
public void cacheEvicted(CacheEntriesEvictedEvent<String, Object> event) {
if (!event.isPre())
for (Map.Entry<String, Object> entry : event.getEntries().entrySet()) {
Object object = entry.getValue();
bumpVersion(entry.getKey());
if (object == null) continue;
RealmCacheManager.logger.tracev("evicting: {0}" + object.getClass().getName());
Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
if (predicate != null) runEvictions(predicate);
protected String generateEventId(InvalidationEvent event) {
return new StringBuilder(event.getId())
.append("_")
.append(event.hashCode())
.toString();
}
protected void invalidationEventReceived(InvalidationEvent event) {
Set<String> invalidations = new HashSet<>();
addInvalidationsFromEvent(event, invalidations);
getLogger().debugf("Invalidating %d cache items after received event %s", invalidations.size(), event);
for (String invalidation : invalidations) {
invalidateObject(invalidation);
}
}
public void runEvictions(Predicate<Map.Entry<String, Revisioned>> current) {
Set<String> evictions = new HashSet<>();
addInvalidations(current, evictions);
RealmCacheManager.logger.tracev("running evictions size: {0}", evictions.size());
for (String key : evictions) {
cache.evict(key);
bumpVersion(key);
}
}
protected abstract void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations);
protected abstract Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object);
}

View file

@ -52,7 +52,7 @@ public class ClientAdapter implements ClientModel {
private void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerClientInvalidation(cached.getId());
cacheSession.registerClientInvalidation(cached.getId(), cached.getClientId(), cachedRealm.getId());
updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm);
if (updated == null) throw new IllegalStateException("Not found in database");
}
@ -577,18 +577,12 @@ public class ClientAdapter implements ClientModel {
@Override
public RoleModel addRole(String name) {
getDelegateForUpdate();
RoleModel role = updated.addRole(name);
cacheSession.registerRoleInvalidation(role.getId());
return role;
return cacheSession.addClientRole(getRealm(), this, name);
}
@Override
public RoleModel addRole(String id, String name) {
getDelegateForUpdate();
RoleModel role = updated.addRole(id, name);
cacheSession.registerRoleInvalidation(role.getId());
return role;
return cacheSession.addClientRole(getRealm(), this, id, name);
}
@Override

View file

@ -21,7 +21,6 @@ import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
@ -29,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheRealmProviderFactory;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -54,14 +54,23 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME);
realmCache = new RealmCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, new ClusterListener() {
@Override
public void run(ClusterEvent event) {
realmCache.clear();
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
if (event instanceof InvalidationEvent) {
InvalidationEvent invalidationEvent = (InvalidationEvent) event;
realmCache.invalidationEventReceived(invalidationEvent);
}
});
cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
realmCache.clear();
});
log.debug("Registered cluster listeners");
}
}
}

View file

@ -21,7 +21,6 @@ import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
@ -29,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.cache.UserCacheProviderFactory;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -55,13 +55,25 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME);
userCache = new UserCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(USER_CLEAR_CACHE_EVENTS, new ClusterListener() {
@Override
public void run(ClusterEvent event) {
userCache.clear();
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
if (event instanceof InvalidationEvent) {
InvalidationEvent invalidationEvent = (InvalidationEvent) event;
userCache.invalidationEventReceived(invalidationEvent);
}
});
cluster.registerListener(USER_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
userCache.clear();
});
log.debug("Registered cluster listeners");
}
}
}

View file

@ -39,13 +39,8 @@ import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.cache.CachedRealmModel;
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.UserStorageProvider;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@ -75,7 +70,7 @@ public class RealmAdapter implements CachedRealmModel {
@Override
public RealmModel getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerRealmInvalidation(cached.getId());
cacheSession.registerRealmInvalidation(cached.getId(), cached.getName());
updated = cacheSession.getDelegate().getRealm(cached.getId());
if (updated == null) throw new IllegalStateException("Not found in database");
}
@ -731,13 +726,6 @@ public class RealmAdapter implements CachedRealmModel {
updated.setNotBefore(notBefore);
}
@Override
public boolean removeRoleById(String id) {
cacheSession.registerRoleInvalidation(id);
getDelegateForUpdate();
return updated.removeRoleById(id);
}
@Override
public boolean isEventsEnabled() {
if (isUpdated()) return updated.isEventsEnabled();
@ -837,18 +825,12 @@ public class RealmAdapter implements CachedRealmModel {
@Override
public RoleModel addRole(String name) {
getDelegateForUpdate();
RoleModel role = updated.addRole(name);
cacheSession.registerRoleInvalidation(role.getId());
return role;
return cacheSession.addRealmRole(this, name);
}
@Override
public RoleModel addRole(String id, String name) {
getDelegateForUpdate();
RoleModel role = updated.addRole(id, name);
cacheSession.registerRoleInvalidation(role.getId());
return role;
return cacheSession.addRealmRole(this, id, name);
}
@Override
@ -1257,12 +1239,6 @@ public class RealmAdapter implements CachedRealmModel {
return cacheSession.createGroup(this, id, name);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
cacheSession.addTopLevelGroup(this, subGroup);
}
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
cacheSession.moveGroup(this, group, toParent);

View file

@ -18,145 +18,88 @@
package org.keycloak.models.cache.infinispan;
import org.infinispan.Cache;
import org.infinispan.notifications.Listener;
import org.jboss.logging.Logger;
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;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.stream.ClientQueryPredicate;
import org.keycloak.models.cache.infinispan.stream.ClientTemplateQueryPredicate;
import org.keycloak.models.cache.infinispan.stream.GroupQueryPredicate;
import org.keycloak.models.cache.infinispan.events.RealmCacheInvalidationEvent;
import org.keycloak.models.cache.infinispan.stream.HasRolePredicate;
import org.keycloak.models.cache.infinispan.stream.InClientPredicate;
import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
import org.keycloak.models.cache.infinispan.stream.RealmQueryPredicate;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Listener
public class RealmCacheManager extends CacheManager {
protected static final Logger logger = Logger.getLogger(RealmCacheManager.class);
private static final Logger logger = Logger.getLogger(RealmCacheManager.class);
@Override
protected Logger getLogger() {
return logger;
}
public RealmCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
super(cache, revisions);
}
public void realmInvalidation(String id, Set<String> invalidations) {
Predicate<Map.Entry<String, Revisioned>> predicate = getRealmInvalidationPredicate(id);
addInvalidations(predicate, invalidations);
public void realmUpdated(String id, String name, Set<String> invalidations) {
invalidations.add(id);
invalidations.add(RealmCacheSession.getRealmByNameCacheKey(name));
}
public Predicate<Map.Entry<String, Revisioned>> getRealmInvalidationPredicate(String id) {
return RealmQueryPredicate.create().realm(id);
public void realmRemoval(String id, String name, Set<String> invalidations) {
realmUpdated(id, name, invalidations);
addInvalidations(InRealmPredicate.create().realm(id), invalidations);
}
public void clientInvalidation(String id, Set<String> invalidations) {
addInvalidations(getClientInvalidationPredicate(id), invalidations);
public void roleAdded(String roleContainerId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
}
public Predicate<Map.Entry<String, Revisioned>> getClientInvalidationPredicate(String id) {
return ClientQueryPredicate.create().client(id);
public void roleUpdated(String roleContainerId, String roleName, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
}
public void roleInvalidation(String id, Set<String> invalidations) {
addInvalidations(getRoleInvalidationPredicate(id), invalidations);
public void roleRemoval(String id, String roleName, String roleContainerId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
addInvalidations(HasRolePredicate.create().role(id), invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getRoleInvalidationPredicate(String id) {
return HasRolePredicate.create().role(id);
public void groupQueriesInvalidations(String realmId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getGroupsQueryCacheKey(realmId));
invalidations.add(RealmCacheSession.getTopGroupsQueryCacheKey(realmId)); // Just easier to always invalidate top-level too. It's not big performance penalty
}
public void groupInvalidation(String id, Set<String> invalidations) {
addInvalidations(getGroupInvalidationPredicate(id), invalidations);
public void clientAdded(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
}
public Predicate<Map.Entry<String, Revisioned>> getGroupInvalidationPredicate(String id) {
return GroupQueryPredicate.create().group(id);
public void clientUpdated(String realmId, String clientUuid, String clientId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
}
public void clientTemplateInvalidation(String id, Set<String> invalidations) {
addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations);
// Client roles invalidated separately
public void clientRemoval(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
addInvalidations(InClientPredicate.create().client(clientUUID), invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getClientTemplateInvalidationPredicate(String id) {
return ClientTemplateQueryPredicate.create().template(id);
}
public void realmRemoval(String id, Set<String> invalidations) {
Predicate<Map.Entry<String, Revisioned>> predicate = getRealmRemovalPredicate(id);
addInvalidations(predicate, invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getRealmRemovalPredicate(String id) {
Predicate<Map.Entry<String, Revisioned>> predicate = null;
predicate = RealmQueryPredicate.create().realm(id)
.or(InRealmPredicate.create().realm(id));
return predicate;
}
public void clientAdded(String realmId, String id, Set<String> invalidations) {
addInvalidations(getClientAddedPredicate(realmId), invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getClientAddedPredicate(String realmId) {
return ClientQueryPredicate.create().inRealm(realmId);
}
public void clientRemoval(String realmId, String id, Set<String> invalidations) {
Predicate<Map.Entry<String, Revisioned>> predicate = null;
predicate = getClientRemovalPredicate(realmId, id);
addInvalidations(predicate, invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getClientRemovalPredicate(String realmId, String id) {
Predicate<Map.Entry<String, Revisioned>> predicate;
predicate = ClientQueryPredicate.create().inRealm(realmId)
.or(ClientQueryPredicate.create().client(id))
.or(InClientPredicate.create().client(id));
return predicate;
}
public void roleRemoval(String id, Set<String> invalidations) {
addInvalidations(getRoleRemovalPredicate(id), invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getRoleRemovalPredicate(String id) {
return getRoleInvalidationPredicate(id);
}
@Override
protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
if (object instanceof CachedRealm) {
CachedRealm cached = (CachedRealm)object;
return getRealmRemovalPredicate(cached.getId());
} else if (object instanceof CachedClient) {
CachedClient cached = (CachedClient)object;
Predicate<Map.Entry<String, Revisioned>> predicate = getClientRemovalPredicate(cached.getRealm(), cached.getId());
return predicate;
} else if (object instanceof CachedRole) {
CachedRole cached = (CachedRole)object;
return getRoleRemovalPredicate(cached.getId());
} else if (object instanceof CachedGroup) {
CachedGroup cached = (CachedGroup)object;
return getGroupInvalidationPredicate(cached.getId());
} else if (object instanceof CachedClientTemplate) {
CachedClientTemplate cached = (CachedClientTemplate)object;
return getClientTemplateInvalidationPredicate(cached.getId());
protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
if (event instanceof RealmCacheInvalidationEvent) {
invalidations.add(event.getId());
((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
return null;
}
}

View file

@ -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.infinispan.events.InvalidationEvent;
import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
@ -38,8 +39,22 @@ import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.models.cache.infinispan.entities.CachedRealmRole;
import org.keycloak.models.cache.infinispan.entities.CachedRole;
import org.keycloak.models.cache.infinispan.entities.ClientListQuery;
import org.keycloak.models.cache.infinispan.entities.GroupListQuery;
import org.keycloak.models.cache.infinispan.entities.RealmListQuery;
import org.keycloak.models.cache.infinispan.entities.RoleListQuery;
import org.keycloak.models.cache.infinispan.events.ClientAddedEvent;
import org.keycloak.models.cache.infinispan.events.ClientRemovedEvent;
import org.keycloak.models.cache.infinispan.events.ClientTemplateEvent;
import org.keycloak.models.cache.infinispan.events.ClientUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.GroupAddedEvent;
import org.keycloak.models.cache.infinispan.events.GroupMovedEvent;
import org.keycloak.models.cache.infinispan.events.GroupRemovedEvent;
import org.keycloak.models.cache.infinispan.events.GroupUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent;
import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.RoleAddedEvent;
import org.keycloak.models.cache.infinispan.events.RoleRemovedEvent;
import org.keycloak.models.cache.infinispan.events.RoleUpdatedEvent;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.HashMap;
@ -126,6 +141,7 @@ public class RealmCacheSession implements CacheRealmProvider {
protected Map<String, GroupAdapter> managedGroups = new HashMap<>();
protected Set<String> listInvalidations = new HashSet<>();
protected Set<String> invalidations = new HashSet<>();
protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
protected boolean clearAll;
protected final long startupRevision;
@ -150,7 +166,7 @@ public class RealmCacheSession implements CacheRealmProvider {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
}
@Override
@ -167,21 +183,19 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
public void registerRealmInvalidation(String id) {
invalidateRealm(id);
cache.realmInvalidation(id, invalidations);
}
private void invalidateRealm(String id) {
invalidations.add(id);
public void registerRealmInvalidation(String id, String name) {
cache.realmUpdated(id, name, invalidations);
RealmAdapter adapter = managedRealms.get(id);
if (adapter != null) adapter.invalidateFlag();
invalidationEvents.add(RealmUpdatedEvent.create(id, name));
}
@Override
public void registerClientInvalidation(String id) {
public void registerClientInvalidation(String id, String clientId, String realmId) {
invalidateClient(id);
cache.clientInvalidation(id, invalidations);
invalidationEvents.add(ClientUpdatedEvent.create(id, clientId, realmId));
cache.clientUpdated(realmId, id, clientId, invalidations);
}
private void invalidateClient(String id) {
@ -193,7 +207,9 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void registerClientTemplateInvalidation(String id) {
invalidateClientTemplate(id);
cache.clientTemplateInvalidation(id, invalidations);
// Note: Adding/Removing client template is supposed to invalidate CachedRealm as well, so the list of clientTemplates is invalidated.
// But separate RealmUpdatedEvent will be sent for it. So ClientTemplateEvent don't need to take care of it.
invalidationEvents.add(ClientTemplateEvent.create(id));
}
private void invalidateClientTemplate(String id) {
@ -203,14 +219,15 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
public void registerRoleInvalidation(String id) {
public void registerRoleInvalidation(String id, String roleName, String roleContainerId) {
invalidateRole(id);
roleInvalidations(id);
cache.roleUpdated(roleContainerId, roleName, invalidations);
invalidationEvents.add(RoleUpdatedEvent.create(id, roleName, roleContainerId));
}
private void roleInvalidations(String roleId) {
private void roleRemovalInvalidations(String roleId, String roleName, String roleContainerId) {
Set<String> newInvalidations = new HashSet<>();
cache.roleInvalidation(roleId, newInvalidations);
cache.roleRemoval(roleId, roleName, roleContainerId, newInvalidations);
invalidations.addAll(newInvalidations);
// need to make sure that scope and group mapping clients and groups are invalidated
for (String id : newInvalidations) {
@ -229,6 +246,11 @@ public class RealmCacheSession implements CacheRealmProvider {
clientTemplate.invalidate();
continue;
}
RoleAdapter role = managedRoles.get(id);
if (role != null) {
role.invalidate();
continue;
}
}
@ -243,10 +265,26 @@ public class RealmCacheSession implements CacheRealmProvider {
if (adapter != null) adapter.invalidate();
}
private void addedRole(String roleId, String roleContainerId) {
// this is needed so that a new role that hasn't been committed isn't cached in a query
listInvalidations.add(roleContainerId);
invalidateRole(roleId);
cache.roleAdded(roleContainerId, invalidations);
invalidationEvents.add(RoleAddedEvent.create(roleId, roleContainerId));
}
@Override
public void registerGroupInvalidation(String id) {
invalidateGroup(id, null, false);
addGroupEventIfAbsent(GroupUpdatedEvent.create(id));
}
private void invalidateGroup(String id, String realmId, boolean invalidateQueries) {
invalidateGroup(id);
cache.groupInvalidation(id, invalidations);
if (invalidateQueries) {
cache.groupQueriesInvalidations(realmId, invalidations);
}
}
private void invalidateGroup(String id) {
@ -259,6 +297,8 @@ public class RealmCacheSession implements CacheRealmProvider {
for (String id : invalidations) {
cache.invalidateObject(id);
}
cache.sendInvalidationEvents(session, invalidationEvents);
}
private KeycloakTransaction getPrepareTransaction() {
@ -358,14 +398,14 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RealmModel createRealm(String name) {
RealmModel realm = getDelegate().createRealm(name);
registerRealmInvalidation(realm.getId());
registerRealmInvalidation(realm.getId(), realm.getName());
return realm;
}
@Override
public RealmModel createRealm(String id, String name) {
RealmModel realm = getDelegate().createRealm(id, name);
registerRealmInvalidation(realm.getId());
registerRealmInvalidation(realm.getId(), realm.getName());
return realm;
}
@ -434,7 +474,7 @@ public class RealmCacheSession implements CacheRealmProvider {
}
}
public String getRealmByNameCacheKey(String name) {
static String getRealmByNameCacheKey(String name) {
return "realm.query.by.name." + name;
}
@ -457,20 +497,12 @@ public class RealmCacheSession implements CacheRealmProvider {
RealmModel realm = getRealm(id);
if (realm == null) return false;
invalidations.add(getRealmClientsQueryCacheKey(id));
invalidations.add(getRealmByNameCacheKey(realm.getName()));
cache.invalidateObject(id);
cache.realmRemoval(id, invalidations);
invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName()));
cache.realmRemoval(id, realm.getName(), invalidations);
return getDelegate().removeRealm(id);
}
protected void invalidateClient(RealmModel realm, ClientModel client) {
invalidateClient(client.getId());
invalidations.add(getRealmClientsQueryCacheKey(realm.getId()));
invalidations.add(getClientByClientIdCacheKey(client.getClientId(), realm));
listInvalidations.add(realm.getId());
}
@Override
public ClientModel addClient(RealmModel realm, String clientId) {
@ -486,30 +518,32 @@ public class RealmCacheSession implements CacheRealmProvider {
private ClientModel addedClient(RealmModel realm, ClientModel client) {
logger.trace("added Client.....");
// need to invalidate realm client query cache every time as it may not be loaded on this node, but loaded on another
invalidateClient(realm, client);
cache.clientAdded(realm.getId(), client.getId(), invalidations);
// this is needed so that a new client that hasn't been committed isn't cached in a query
invalidateClient(client.getId());
// this is needed so that a client that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
invalidationEvents.add(ClientAddedEvent.create(client.getId(), client.getClientId(), realm.getId()));
cache.clientAdded(realm.getId(), client.getId(), client.getClientId(), invalidations);
return client;
}
private String getRealmClientsQueryCacheKey(String realm) {
static String getRealmClientsQueryCacheKey(String realm) {
return realm + REALM_CLIENTS_QUERY_SUFFIX;
}
private String getGroupsQueryCacheKey(String realm) {
static String getGroupsQueryCacheKey(String realm) {
return realm + ".groups";
}
private String getTopGroupsQueryCacheKey(String realm) {
static String getTopGroupsQueryCacheKey(String realm) {
return realm + ".top.groups";
}
private String getRolesCacheKey(String container) {
static String getRolesCacheKey(String container) {
return container + ROLES_QUERY_SUFFIX;
}
private String getRoleByNameCacheKey(String container, String name) {
static String getRoleByNameCacheKey(String container, String name) {
return container + "." + name + ROLES_QUERY_SUFFIX;
}
@ -541,6 +575,7 @@ public class RealmCacheSession implements CacheRealmProvider {
for (String id : query.getClients()) {
ClientModel client = session.realms().getClientById(id, realm);
if (client == null) {
// TODO: Handle with cluster invalidations too
invalidations.add(cacheKey);
return getDelegate().getClients(realm);
}
@ -554,12 +589,16 @@ public class RealmCacheSession implements CacheRealmProvider {
public boolean removeClient(String id, RealmModel realm) {
ClientModel client = getClientById(id, realm);
if (client == null) return false;
// need to invalidate realm client query cache every time client list is changed
invalidateClient(realm, client);
cache.clientRemoval(realm.getId(), id, invalidations);
invalidateClient(client.getId());
// this is needed so that a client that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
invalidationEvents.add(ClientRemovedEvent.create(client));
cache.clientRemoval(realm.getId(), id, client.getClientId(), invalidations);
for (RoleModel role : client.getRoles()) {
String roleId = role.getId();
roleInvalidations(roleId);
roleRemovalInvalidations(role.getId(), role.getName(), client.getId());
}
return getDelegate().removeClient(id, realm);
}
@ -577,11 +616,8 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RoleModel addRealmRole(RealmModel realm, String id, String name) {
invalidations.add(getRolesCacheKey(realm.getId()));
// this is needed so that a new role that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
RoleModel role = getDelegate().addRealmRole(realm, name);
invalidations.add(role.getId());
addedRole(role.getId(), realm.getId());
return role;
}
@ -664,11 +700,8 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) {
invalidations.add(getRolesCacheKey(client.getId()));
// this is needed so that a new role that hasn't been committed isn't cached in a query
listInvalidations.add(client.getId());
RoleModel role = getDelegate().addClientRole(realm, client, id, name);
invalidateRole(role.getId());
addedRole(role.getId(), client.getId());
return role;
}
@ -734,10 +767,12 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public boolean removeRole(RealmModel realm, RoleModel role) {
invalidations.add(getRolesCacheKey(role.getContainer().getId()));
invalidations.add(getRoleByNameCacheKey(role.getContainer().getId(), role.getName()));
listInvalidations.add(role.getContainer().getId());
registerRoleInvalidation(role.getId());
invalidateRole(role.getId());
invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId()));
roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId());
return getDelegate().removeRole(realm, role);
}
@ -797,8 +832,11 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
registerGroupInvalidation(group.getId());
if (toParent != null) registerGroupInvalidation(toParent.getId());
invalidateGroup(group.getId(), realm.getId(), true);
if (toParent != null) invalidateGroup(group.getId(), realm.getId(), false); // Queries already invalidated
listInvalidations.add(realm.getId());
invalidationEvents.add(GroupMovedEvent.create(group, toParent, realm.getId()));
getDelegate().moveGroup(realm, group, toParent);
}
@ -876,14 +914,15 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public boolean removeGroup(RealmModel realm, GroupModel group) {
registerGroupInvalidation(group.getId());
invalidateGroup(group.getId(), realm.getId(), true);
listInvalidations.add(realm.getId());
invalidations.add(getGroupsQueryCacheKey(realm.getId()));
if (group.getParentId() == null) {
invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
} else {
registerGroupInvalidation(group.getParentId());
cache.groupQueriesInvalidations(realm.getId(), invalidations);
if (group.getParentId() != null) {
invalidateGroup(group.getParentId(), realm.getId(), false); // Queries already invalidated
}
invalidationEvents.add(GroupRemovedEvent.create(group, realm.getId()));
return getDelegate().removeGroup(realm, group);
}
@ -893,11 +932,11 @@ public class RealmCacheSession implements CacheRealmProvider {
return groupAdded(realm, group);
}
public GroupModel groupAdded(RealmModel realm, GroupModel group) {
private GroupModel groupAdded(RealmModel realm, GroupModel group) {
listInvalidations.add(realm.getId());
invalidations.add(getGroupsQueryCacheKey(realm.getId()));
invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
cache.groupQueriesInvalidations(realm.getId(), invalidations);
invalidations.add(group.getId());
invalidationEvents.add(GroupAddedEvent.create(group.getId(), realm.getId()));
return group;
}
@ -909,15 +948,32 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void addTopLevelGroup(RealmModel realm, GroupModel subGroup) {
invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
invalidations.add(subGroup.getId());
invalidateGroup(subGroup.getId(), realm.getId(), true);
if (subGroup.getParentId() != null) {
registerGroupInvalidation(subGroup.getParentId());
invalidateGroup(subGroup.getParentId(), realm.getId(), false); // Queries already invalidated
}
addGroupEventIfAbsent(GroupMovedEvent.create(subGroup, null, realm.getId()));
getDelegate().addTopLevelGroup(realm, subGroup);
}
private void addGroupEventIfAbsent(InvalidationEvent eventToAdd) {
String groupId = eventToAdd.getId();
// Check if we have existing event with bigger priority
boolean eventAlreadyExists = invalidationEvents.stream().filter((InvalidationEvent event) -> {
return (event.getId().equals(groupId)) && (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent);
}).findFirst().isPresent();
if (!eventAlreadyExists) {
invalidationEvents.add(eventToAdd);
}
}
@Override
public ClientModel getClientById(String id, RealmModel realm) {
CachedClient cached = cache.get(id, CachedClient.class);
@ -948,7 +1004,7 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public ClientModel getClientByClientId(String clientId, RealmModel realm) {
String cacheKey = getClientByClientIdCacheKey(clientId, realm);
String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());
ClientListQuery query = cache.get(cacheKey, ClientListQuery.class);
String id = null;
@ -976,8 +1032,8 @@ public class RealmCacheSession implements CacheRealmProvider {
return getClientById(id, realm);
}
public String getClientByClientIdCacheKey(String clientId, RealmModel realm) {
return realm.getId() + ".client.query.by.clientId." + clientId;
static String getClientByClientIdCacheKey(String clientId, String realmId) {
return realmId + ".client.query.by.clientId." + clientId;
}
@Override

View file

@ -47,7 +47,7 @@ public class RoleAdapter implements RoleModel {
protected void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerRoleInvalidation(cached.getId());
cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId());
updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm);
if (updated == null) throw new IllegalStateException("Not found in database");
}

View file

@ -18,40 +18,94 @@
package org.keycloak.models.cache.infinispan;
import org.infinispan.Cache;
import org.infinispan.notifications.Listener;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.events.UserCacheInvalidationEvent;
import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Listener
public class UserCacheManager extends CacheManager {
protected static final Logger logger = Logger.getLogger(UserCacheManager.class);
private static final Logger logger = Logger.getLogger(UserCacheManager.class);
protected volatile boolean enabled = true;
public UserCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
super(cache, revisions);
}
@Override
protected Logger getLogger() {
return logger;
}
@Override
public void clear() {
cache.clear();
revisions.clear();
}
public void userUpdatedInvalidations(String userId, String username, String email, String realmId, Set<String> invalidations) {
invalidations.add(userId);
if (email != null) invalidations.add(UserCacheSession.getUserByEmailCacheKey(realmId, email));
invalidations.add(UserCacheSession.getUserByUsernameCacheKey(realmId, username));
}
// Fully invalidate user including consents and federatedIdentity links.
public void fullUserInvalidation(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Map<String, String> federatedIdentities, Set<String> invalidations) {
userUpdatedInvalidations(userId, username, email, realmId, invalidations);
if (identityFederationEnabled) {
// Invalidate all keys for lookup this user by any identityProvider link
for (Map.Entry<String, String> socialLink : federatedIdentities.entrySet()) {
String fedIdentityCacheKey = UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, socialLink.getKey(), socialLink.getValue());
invalidations.add(fedIdentityCacheKey);
}
// Invalidate federationLinks of user
invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
}
// Consents
invalidations.add(UserCacheSession.getConsentCacheKey(userId));
}
public void federatedIdentityLinkUpdatedInvalidation(String userId, Set<String> invalidations) {
invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
}
public void federatedIdentityLinkRemovedInvalidation(String userId, String realmId, String identityProviderId, String socialUserId, Set<String> invalidations) {
invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
if (identityProviderId != null) {
invalidations.add(UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, identityProviderId, socialUserId));
}
}
public void consentInvalidation(String userId, Set<String> invalidations) {
invalidations.add(UserCacheSession.getConsentCacheKey(userId));
}
@Override
protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
return null;
protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
if (event instanceof UserCacheInvalidationEvent) {
((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
}
public void invalidateRealmUsers(String realm, Set<String> invalidations) {
addInvalidations(InRealmPredicate.create().realm(realm), invalidations);
InRealmPredicate inRealmPredicate = getInRealmPredicate(realm);
addInvalidations(inRealmPredicate, invalidations);
}
private InRealmPredicate getInRealmPredicate(String realmId) {
return InRealmPredicate.create().realm(realmId);
}
}

View file

@ -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.infinispan.events.InvalidationEvent;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
@ -42,6 +43,12 @@ 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.models.cache.infinispan.events.UserCacheRealmInvalidationEvent;
import org.keycloak.models.cache.infinispan.events.UserConsentsUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.UserFederationLinkRemovedEvent;
import org.keycloak.models.cache.infinispan.events.UserFederationLinkUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent;
import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
@ -72,6 +79,7 @@ public class UserCacheSession implements UserCache {
protected Set<String> invalidations = new HashSet<>();
protected Set<String> realmInvalidations = new HashSet<>();
protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
protected Map<String, UserModel> managedUsers = new HashMap<>();
public UserCacheSession(UserCacheManager cache, KeycloakSession session) {
@ -85,7 +93,7 @@ public class UserCacheSession implements UserCache {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
}
public UserProvider getDelegate() {
@ -97,10 +105,8 @@ public class UserCacheSession implements UserCache {
}
public void registerUserInvalidation(RealmModel realm,CachedUser user) {
invalidations.add(user.getId());
if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), user.getRealm(), invalidations);
invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), user.getRealm()));
}
@Override
@ -108,10 +114,8 @@ public class UserCacheSession implements UserCache {
if (user instanceof CachedUserModel) {
((CachedUserModel)user).invalidate();
} else {
invalidations.add(user.getId());
if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), invalidations);
invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId()));
}
}
@ -127,6 +131,8 @@ public class UserCacheSession implements UserCache {
for (String invalidation : invalidations) {
cache.invalidateObject(invalidation);
}
cache.sendInvalidationEvents(session, invalidationEvents);
}
private KeycloakTransaction getTransaction() {
@ -201,19 +207,23 @@ public class UserCacheSession implements UserCache {
return adapter;
}
public String getUserByUsernameCacheKey(String realmId, String username) {
static String getUserByUsernameCacheKey(String realmId, String username) {
return realmId + ".username." + username;
}
public String getUserByEmailCacheKey(String realmId, String email) {
static String getUserByEmailCacheKey(String realmId, String email) {
return realmId + ".email." + email;
}
public String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
return realmId + ".idp." + socialLink.getIdentityProvider() + "." + socialLink.getUserId();
private static String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
return getUserByFederatedIdentityCacheKey(realmId, socialLink.getIdentityProvider(), socialLink.getUserId());
}
public String getFederatedIdentityLinksCacheKey(String userId) {
static String getUserByFederatedIdentityCacheKey(String realmId, String identityProvider, String socialUserId) {
return realmId + ".idp." + identityProvider + "." + socialUserId;
}
static String getFederatedIdentityLinksCacheKey(String userId) {
return userId + ".idplinks";
}
@ -655,27 +665,32 @@ public class UserCacheSession implements UserCache {
@Override
public void updateConsent(RealmModel realm, String userId, UserConsentModel consent) {
invalidations.add(getConsentCacheKey(userId));
invalidateConsent(userId);
getDelegate().updateConsent(realm, userId, consent);
}
@Override
public boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId) {
invalidations.add(getConsentCacheKey(userId));
invalidateConsent(userId);
return getDelegate().revokeConsentForClient(realm, userId, clientInternalId);
}
public String getConsentCacheKey(String userId) {
static String getConsentCacheKey(String userId) {
return userId + ".consents";
}
@Override
public void addConsent(RealmModel realm, String userId, UserConsentModel consent) {
invalidations.add(getConsentCacheKey(userId));
invalidateConsent(userId);
getDelegate().addConsent(realm, userId, consent);
}
private void invalidateConsent(String userId) {
cache.consentInvalidation(userId, invalidations);
invalidationEvents.add(UserConsentsUpdatedEvent.create(userId));
}
@Override
public UserConsentModel getConsentByClient(RealmModel realm, String userId, String clientId) {
logger.tracev("getConsentByClient: {0}", userId);
@ -754,7 +769,7 @@ public class UserCacheSession implements UserCache {
public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) {
UserModel user = getDelegate().addUser(realm, id, username, addDefaultRoles, addDefaultRoles);
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
invalidateUser(realm, user);
fullyInvalidateUser(realm, user);
managedUsers.put(user.getId(), user);
return user;
}
@ -763,94 +778,89 @@ public class UserCacheSession implements UserCache {
public UserModel addUser(RealmModel realm, String username) {
UserModel user = getDelegate().addUser(realm, username);
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
invalidateUser(realm, user);
fullyInvalidateUser(realm, user);
managedUsers.put(user.getId(), user);
return user;
}
protected void invalidateUser(RealmModel realm, UserModel user) {
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
protected void fullyInvalidateUser(RealmModel realm, UserModel user) {
Set<FederatedIdentityModel> federatedIdentities = realm.isIdentityFederationEnabled() ? getFederatedIdentities(user, realm) : null;
if (realm.isIdentityFederationEnabled()) {
// Invalidate all keys for lookup this user by any identityProvider link
Set<FederatedIdentityModel> federatedIdentities = getFederatedIdentities(user, realm);
for (FederatedIdentityModel socialLink : federatedIdentities) {
String fedIdentityCacheKey = getUserByFederatedIdentityCacheKey(realm.getId(), socialLink);
invalidations.add(fedIdentityCacheKey);
}
UserFullInvalidationEvent event = UserFullInvalidationEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), federatedIdentities);
// Invalidate federationLinks of user
invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
}
invalidations.add(user.getId());
if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
cache.fullUserInvalidation(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), event.getFederatedIdentities(), invalidations);
invalidationEvents.add(event);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
invalidateUser(realm, user);
fullyInvalidateUser(realm, user);
return getDelegate().removeUser(realm, user);
}
@Override
public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink) {
invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
invalidateFederationLink(user.getId());
getDelegate().addFederatedIdentity(realm, user, socialLink);
}
@Override
public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
invalidations.add(getFederatedIdentityLinksCacheKey(federatedUser.getId()));
invalidateFederationLink(federatedUser.getId());
getDelegate().updateFederatedIdentity(realm, federatedUser, federatedIdentityModel);
}
private void invalidateFederationLink(String userId) {
cache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
invalidationEvents.add(UserFederationLinkUpdatedEvent.create(userId));
}
@Override
public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider) {
// Needs to invalidate both directions
FederatedIdentityModel socialLink = getFederatedIdentity(user, socialProvider, realm);
invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
if (socialLink != null) {
invalidations.add(getUserByFederatedIdentityCacheKey(realm.getId(), socialLink));
}
UserFederationLinkRemovedEvent event = UserFederationLinkRemovedEvent.create(user.getId(), realm.getId(), socialLink);
cache.federatedIdentityLinkRemovedInvalidation(user.getId(), realm.getId(), event.getIdentityProviderId(), event.getSocialUserId(), invalidations);
invalidationEvents.add(event);
return getDelegate().removeFederatedIdentity(realm, user, socialProvider);
}
@Override
public void grantToAllUsers(RealmModel realm, RoleModel role) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().grantToAllUsers(realm, role);
}
@Override
public void preRemove(RealmModel realm) {
realmInvalidations.add(realm.getId());
addRealmInvalidation(realm.getId());
getDelegate().preRemove(realm);
}
@Override
public void preRemove(RealmModel realm, RoleModel role) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, role);
}
@Override
public void preRemove(RealmModel realm, GroupModel group) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, group);
}
@Override
public void preRemove(RealmModel realm, UserFederationProviderModel link) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, link);
}
@Override
public void preRemove(RealmModel realm, ClientModel client) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, client);
}
@ -862,9 +872,14 @@ public class UserCacheSession implements UserCache {
@Override
public void preRemove(RealmModel realm, ComponentModel component) {
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, component);
}
private void addRealmInvalidation(String realmId) {
realmInvalidations.add(realmId);
invalidationEvents.add(UserCacheRealmInvalidationEvent.create(realmId));
}
}

View file

@ -135,7 +135,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
}
protected List<String> defaultGroups = new LinkedList<String>();
protected Set<String> groups = new HashSet<String>();
protected List<String> clientTemplates= new LinkedList<>();
protected boolean internationalizationEnabled;
protected Set<String> supportedLocales;
@ -237,9 +236,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
executionsById.put(execution.getId(), execution);
}
}
for (GroupModel group : model.getGroups()) {
groups.add(group.getId());
}
for (AuthenticatorConfigModel authenticator : model.getAuthenticatorConfigs()) {
authenticatorConfigs.put(authenticator.getId(), authenticator);
}
@ -541,10 +538,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return clientAuthenticationFlow;
}
public Set<String> getGroups() {
return groups;
}
public List<String> getDefaultGroups() {
return defaultGroups;
}

View file

@ -1,11 +0,0 @@
package org.keycloak.models.cache.infinispan.entities;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ClientTemplateQuery extends InRealm {
Set<String> getTemplates();
}

View file

@ -1,8 +1,6 @@
package org.keycloak.models.cache.infinispan;
package org.keycloak.models.cache.infinispan.entities;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
import org.keycloak.models.cache.infinispan.entities.GroupQuery;
import java.util.Set;

View file

@ -59,7 +59,8 @@ public class RoleListQuery extends AbstractRevisioned implements RoleQuery, InCl
public String toString() {
return "RoleListQuery{" +
"id='" + getId() + "'" +
"realmName='" + realmName + '\'' +
", realmName='" + realmName + '\'' +
", clientUuid='" + client + '\'' +
'}';
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientUuid;
private String clientId;
private String realmId;
public static ClientAddedEvent create(String clientUuid, String clientId, String realmId) {
ClientAddedEvent event = new ClientAddedEvent();
event.clientUuid = clientUuid;
event.clientId = clientId;
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return clientUuid;
}
@Override
public String toString() {
return String.format("ClientAddedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.clientAdded(realmId, clientUuid, clientId, invalidations);
}
}

View file

@ -0,0 +1,74 @@
/*
* 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.events;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientUuid;
private String clientId;
private String realmId;
// roleId -> roleName
private Map<String, String> clientRoles;
public static ClientRemovedEvent create(ClientModel client) {
ClientRemovedEvent event = new ClientRemovedEvent();
event.realmId = client.getRealm().getId();
event.clientUuid = client.getId();
event.clientId = client.getClientId();
event.clientRoles = new HashMap<>();
for (RoleModel clientRole : client.getRoles()) {
event.clientRoles.put(clientRole.getId(), clientRole.getName());
}
return event;
}
@Override
public String getId() {
return clientUuid;
}
@Override
public String toString() {
return String.format("ClientRemovedEvent [ realmId=%s, clientUuid=%s, clientId=%s, clientRoleIds=%s ]", realmId, clientUuid, clientId, clientRoles);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.clientRemoval(realmId, clientUuid, clientId, invalidations);
// Separate iteration for all client roles to invalidate records dependent on them
for (Map.Entry<String, String> clientRole : clientRoles.entrySet()) {
String roleId = clientRole.getKey();
String roleName = clientRole.getValue();
realmCache.roleRemoval(roleId, roleName, clientUuid, invalidations);
}
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientTemplateEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientTemplateId;
public static ClientTemplateEvent create(String clientTemplateId) {
ClientTemplateEvent event = new ClientTemplateEvent();
event.clientTemplateId = clientTemplateId;
return event;
}
@Override
public String getId() {
return clientTemplateId;
}
@Override
public String toString() {
return "ClientTemplateEvent [ " + clientTemplateId + " ]";
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
// Nothing. ID was already invalidated
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientUuid;
private String clientId;
private String realmId;
public static ClientUpdatedEvent create(String clientUuid, String clientId, String realmId) {
ClientUpdatedEvent event = new ClientUpdatedEvent();
event.clientUuid = clientUuid;
event.clientId = clientId;
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return clientUuid;
}
@Override
public String toString() {
return String.format("ClientUpdatedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.clientUpdated(realmId, clientUuid, clientId, invalidations);
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String groupId;
private String realmId;
public static GroupAddedEvent create(String groupId, String realmId) {
GroupAddedEvent event = new GroupAddedEvent();
event.realmId = realmId;
event.groupId = groupId;
return event;
}
@Override
public String getId() {
return groupId;
}
@Override
public String toString() {
return String.format("GroupAddedEvent [ realmId=%s, groupId=%s ]", realmId, groupId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations);
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.GroupModel;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupMovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String groupId;
private String newParentId; // null if moving to top-level
private String oldParentId; // null if moving from top-level
private String realmId;
public static GroupMovedEvent create(GroupModel group, GroupModel toParent, String realmId) {
GroupMovedEvent event = new GroupMovedEvent();
event.realmId = realmId;
event.groupId = group.getId();
event.oldParentId = group.getParentId();
event.newParentId = toParent==null ? null : toParent.getId();
return event;
}
@Override
public String getId() {
return groupId;
}
@Override
public String toString() {
return String.format("GroupMovedEvent [ realmId=%s, groupId=%s, newParentId=%s, oldParentId=%s ]", realmId, groupId, newParentId, oldParentId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations);
if (newParentId != null) {
invalidations.add(newParentId);
}
if (oldParentId != null) {
invalidations.add(oldParentId);
}
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.GroupModel;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String groupId;
private String parentId;
private String realmId;
public static GroupRemovedEvent create(GroupModel group, String realmId) {
GroupRemovedEvent event = new GroupRemovedEvent();
event.realmId = realmId;
event.groupId = group.getId();
event.parentId = group.getParentId();
return event;
}
@Override
public String getId() {
return groupId;
}
@Override
public String toString() {
return String.format("GroupRemovedEvent [ realmId=%s, groupId=%s, parentId=%s ]", realmId, groupId, parentId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations);
if (parentId != null) {
invalidations.add(parentId);
}
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String groupId;
public static GroupUpdatedEvent create(String groupId) {
GroupUpdatedEvent event = new GroupUpdatedEvent();
event.groupId = groupId;
return event;
}
@Override
public String getId() {
return groupId;
}
@Override
public String toString() {
return "GroupUpdatedEvent [ " + groupId + " ]";
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
// Nothing. ID already invalidated
}
}

View file

@ -0,0 +1,43 @@
/*
* 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.events;
import org.keycloak.cluster.ClusterEvent;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class InvalidationEvent implements ClusterEvent {
public abstract String getId();
@Override
public int hashCode() {
return getClass().hashCode() * 13 + getId().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (!obj.getClass().equals(this.getClass())) return false;
InvalidationEvent that = (InvalidationEvent) obj;
if (!that.getId().equals(getId())) return false;
return true;
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface RealmCacheInvalidationEvent {
void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations);
}

View file

@ -0,0 +1,53 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RealmRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String realmId;
private String realmName;
public static RealmRemovedEvent create(String realmId, String realmName) {
RealmRemovedEvent event = new RealmRemovedEvent();
event.realmId = realmId;
event.realmName = realmName;
return event;
}
@Override
public String getId() {
return realmId;
}
@Override
public String toString() {
return String.format("RealmRemovedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.realmRemoval(realmId, realmName, invalidations);
}
}

View file

@ -0,0 +1,53 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RealmUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String realmId;
private String realmName;
public static RealmUpdatedEvent create(String realmId, String realmName) {
RealmUpdatedEvent event = new RealmUpdatedEvent();
event.realmId = realmId;
event.realmName = realmName;
return event;
}
@Override
public String getId() {
return realmId;
}
@Override
public String toString() {
return String.format("RealmUpdatedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.realmUpdated(realmId, realmName, invalidations);
}
}

View file

@ -0,0 +1,53 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String roleId;
private String containerId;
public static RoleAddedEvent create(String roleId, String containerId) {
RoleAddedEvent event = new RoleAddedEvent();
event.roleId = roleId;
event.containerId = containerId;
return event;
}
@Override
public String getId() {
return roleId;
}
@Override
public String toString() {
return String.format("RoleAddedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.roleAdded(containerId, invalidations);
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String roleId;
private String roleName;
private String containerId;
public static RoleRemovedEvent create(String roleId, String roleName, String containerId) {
RoleRemovedEvent event = new RoleRemovedEvent();
event.roleId = roleId;
event.roleName = roleName;
event.containerId = containerId;
return event;
}
@Override
public String getId() {
return roleId;
}
@Override
public String toString() {
return String.format("RoleRemovedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.roleRemoval(roleId, roleName, containerId, invalidations);
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String roleId;
private String roleName;
private String containerId;
public static RoleUpdatedEvent create(String roleId, String roleName, String containerId) {
RoleUpdatedEvent event = new RoleUpdatedEvent();
event.roleId = roleId;
event.roleName = roleName;
event.containerId = containerId;
return event;
}
@Override
public String getId() {
return roleId;
}
@Override
public String toString() {
return String.format("RoleUpdatedEvent [ roleId=%s, roleName=%s, containerId=%s ]", roleId, roleName, containerId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.roleUpdated(containerId, roleName, invalidations);
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface UserCacheInvalidationEvent {
void addInvalidations(UserCacheManager userCache, Set<String> invalidations);
}

View file

@ -0,0 +1,51 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserCacheRealmInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String realmId;
public static UserCacheRealmInvalidationEvent create(String realmId) {
UserCacheRealmInvalidationEvent event = new UserCacheRealmInvalidationEvent();
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return realmId; // Just a placeholder
}
@Override
public String toString() {
return String.format("UserCacheRealmInvalidationEvent [ realmId=%s ]", realmId);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.invalidateRealmUsers(realmId, invalidations);
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserConsentsUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
public static UserConsentsUpdatedEvent create(String userId) {
UserConsentsUpdatedEvent event = new UserConsentsUpdatedEvent();
event.userId = userId;
return event;
}
@Override
public String getId() {
return userId;
}
@Override
public String toString() {
return String.format("UserConsentsUpdatedEvent [ userId=%s ]", userId);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.consentInvalidation(userId, invalidations);
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserFederationLinkRemovedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
private String realmId;
private String identityProviderId;
private String socialUserId;
public static UserFederationLinkRemovedEvent create(String userId, String realmId, FederatedIdentityModel socialLink) {
UserFederationLinkRemovedEvent event = new UserFederationLinkRemovedEvent();
event.userId = userId;
event.realmId = realmId;
if (socialLink != null) {
event.identityProviderId = socialLink.getIdentityProvider();
event.socialUserId = socialLink.getUserId();
}
return event;
}
@Override
public String getId() {
return userId;
}
public String getRealmId() {
return realmId;
}
public String getIdentityProviderId() {
return identityProviderId;
}
public String getSocialUserId() {
return socialUserId;
}
@Override
public String toString() {
return String.format("UserFederationLinkRemovedEvent [ userId=%s, identityProviderId=%s, socialUserId=%s ]", userId, identityProviderId, socialUserId);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.federatedIdentityLinkRemovedInvalidation(userId, realmId, identityProviderId, socialUserId, invalidations);
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserFederationLinkUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
public static UserFederationLinkUpdatedEvent create(String userId) {
UserFederationLinkUpdatedEvent event = new UserFederationLinkUpdatedEvent();
event.userId = userId;
return event;
}
@Override
public String getId() {
return userId;
}
@Override
public String toString() {
return String.format("UserFederationLinkUpdatedEvent [ userId=%s ]", userId);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
}
}

View file

@ -0,0 +1,78 @@
/*
* 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.events;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* Used when user added/removed
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserFullInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
private String username;
private String email;
private String realmId;
private boolean identityFederationEnabled;
private Map<String, String> federatedIdentities;
public static UserFullInvalidationEvent create(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Collection<FederatedIdentityModel> federatedIdentities) {
UserFullInvalidationEvent event = new UserFullInvalidationEvent();
event.userId = userId;
event.username = username;
event.email = email;
event.realmId = realmId;
event.identityFederationEnabled = identityFederationEnabled;
if (identityFederationEnabled) {
event.federatedIdentities = new HashMap<>();
for (FederatedIdentityModel socialLink : federatedIdentities) {
event.federatedIdentities.put(socialLink.getIdentityProvider(), socialLink.getUserId());
}
}
return event;
}
@Override
public String getId() {
return userId;
}
public Map<String, String> getFederatedIdentities() {
return federatedIdentities;
}
@Override
public String toString() {
return String.format("UserFullInvalidationEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.fullUserInvalidation(userId, username, email, realmId, identityFederationEnabled, federatedIdentities, invalidations);
}
}

View file

@ -0,0 +1,57 @@
/*
* 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.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
private String username;
private String email;
private String realmId;
public static UserUpdatedEvent create(String userId, String username, String email, String realmId) {
UserUpdatedEvent event = new UserUpdatedEvent();
event.userId = userId;
event.username = username;
event.email = email;
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return userId;
}
@Override
public String toString() {
return String.format("UserUpdatedEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.userUpdatedInvalidations(userId, username, email, realmId, invalidations);
}
}

View file

@ -1,48 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.infinispan.entities.ClientQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClientQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
protected static final Logger logger = Logger.getLogger(ClientQueryPredicate.class);
private String client;
private String inRealm;
public static ClientQueryPredicate create() {
return new ClientQueryPredicate();
}
public ClientQueryPredicate client(String client) {
this.client = client;
return this;
}
public ClientQueryPredicate inRealm(String inRealm) {
this.inRealm = inRealm;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof ClientQuery)) return false;
ClientQuery query = (ClientQuery)value;
if (client != null && !query.getClients().contains(client)) return false;
if (inRealm != null && !query.getRealm().equals(inRealm)) return false;
return true;
}
}

View file

@ -1,40 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.keycloak.models.cache.infinispan.entities.ClientTemplateQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClientTemplateQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String template;
public static ClientTemplateQueryPredicate create() {
return new ClientTemplateQueryPredicate();
}
public ClientTemplateQueryPredicate template(String template) {
this.template = template;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof ClientTemplateQuery)) return false;
ClientTemplateQuery query = (ClientTemplateQuery)value;
return query.getTemplates().contains(template);
}
}

View file

@ -1,40 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.keycloak.models.cache.infinispan.entities.GroupQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String group;
public static GroupQueryPredicate create() {
return new GroupQueryPredicate();
}
public GroupQueryPredicate group(String group) {
this.group = group;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof GroupQuery)) return false;
GroupQuery query = (GroupQuery)value;
return query.getGroups().contains(group);
}
}

View file

@ -1,40 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.keycloak.models.cache.infinispan.entities.RealmQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RealmQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String realm;
public static RealmQueryPredicate create() {
return new RealmQueryPredicate();
}
public RealmQueryPredicate realm(String realm) {
this.realm = realm;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof RealmQuery)) return false;
RealmQuery query = (RealmQuery)value;
return query.getRealms().contains(realm);
}
}

View file

@ -1,40 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.entities.RoleQuery;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RoleQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String role;
public static RoleQueryPredicate create() {
return new RoleQueryPredicate();
}
public RoleQueryPredicate role(String role) {
this.role = role;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof RoleQuery)) return false;
RoleQuery query = (RoleQuery)value;
return query.getRoles().contains(role);
}
}

View file

@ -92,13 +92,13 @@ public class InfinispanUserSessionInitializer {
private boolean isFinished() {
InitializerState state = (InitializerState) workCache.get(stateKey);
InitializerState state = getStateFromCache();
return state != null && state.isFinished();
}
private InitializerState getOrCreateInitializerState() {
InitializerState state = (InitializerState) workCache.get(stateKey);
InitializerState state = getStateFromCache();
if (state == null) {
final int[] count = new int[1];
@ -128,6 +128,12 @@ public class InfinispanUserSessionInitializer {
}
private InitializerState getStateFromCache() {
// TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
return (InitializerState) workCache.getAdvancedCache()
.withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.get(stateKey);
}
private void saveStateToCache(final InitializerState state) {
@ -138,8 +144,9 @@ public class InfinispanUserSessionInitializer {
public void run() {
// Save this synchronously to ensure all nodes read correct state
// TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
InfinispanUserSessionInitializer.this.workCache.getAdvancedCache().
withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.put(stateKey, state);
}

View file

@ -0,0 +1,231 @@
/*
* 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.cluster.infinispan;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.Flag;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.persistence.remote.configuration.ExhaustedAction;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.junit.Ignore;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
/**
* Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@Ignore
public class ConcurrencyJDGRemoteCacheTest {
private static Map<String, EntryInfo> state = new HashMap<>();
public static void main(String[] args) throws Exception {
// Init map somehow
for (int i=0 ; i<100 ; i++) {
String key = "key-" + i;
state.put(key, new EntryInfo());
}
// Create caches, listeners and finally worker threads
Worker worker1 = createWorker(1);
Worker worker2 = createWorker(2);
// Start and join workers
worker1.start();
worker2.start();
worker1.join();
worker2.join();
// Output
for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
System.out.println(entry.getKey() + ":::" + entry.getValue());
worker1.cache.remove(entry.getKey());
}
// Finish JVM
worker1.cache.getCacheManager().stop();
worker2.cache.getCacheManager().stop();
}
private static Worker createWorker(int threadId) {
EmbeddedCacheManager manager = createManager(threadId);
Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
System.out.println("Retrieved cache: " + threadId);
RemoteStore remoteStore = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next();
HotRodListener listener = new HotRodListener();
remoteStore.getRemoteCache().addClientListener(listener);
return new Worker(cache, threadId);
}
private static EmbeddedCacheManager createManager(int threadId) {
System.setProperty("java.net.preferIPv4Stack", "true");
System.setProperty("jgroups.tcp.port", "53715");
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = false;
boolean async = false;
boolean allowDuplicateJMXDomains = true;
if (clustered) {
gcb = gcb.clusteredDefault();
gcb.transport().clusterName("test-clustering");
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId);
cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, invalidationCacheConfiguration);
return cacheManager;
}
private static Configuration getCacheBackedByRemoteStore(int threadId) {
ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
// int port = threadId==1 ? 11222 : 11322;
int port = 11222;
return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class)
.fetchPersistentState(false)
.ignoreModifications(false)
.purgeOnStartup(false)
.preload(false)
.shared(true)
.remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
.rawValues(true)
.forceReturnValues(false)
.addServer()
.host("localhost")
.port(port)
.connectionPool()
.maxActive(20)
.exhaustedAction(ExhaustedAction.CREATE_NEW)
.async()
. enabled(false).build();
}
@ClientListener
public static class HotRodListener {
//private AtomicInteger listenerCount = new AtomicInteger(0);
@ClientCacheEntryCreated
public void created(ClientCacheEntryCreatedEvent event) {
String cacheKey = (String) event.getKey();
state.get(cacheKey).successfulListenerWrites.incrementAndGet();
}
@ClientCacheEntryModified
public void updated(ClientCacheEntryModifiedEvent event) {
String cacheKey = (String) event.getKey();
state.get(cacheKey).successfulListenerWrites.incrementAndGet();
}
}
private static class Worker extends Thread {
private final Cache<String, Integer> cache;
private final int myThreadId;
private Worker(Cache<String, Integer> cache, int myThreadId) {
this.cache = cache;
this.myThreadId = myThreadId;
}
@Override
public void run() {
for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
String cacheKey = entry.getKey();
EntryInfo wrapper = state.get(cacheKey);
int val = getClusterStartupTime(this.cache, cacheKey, wrapper);
if (myThreadId == 1) {
wrapper.th1.set(val);
} else {
wrapper.th2.set(val);
}
}
System.out.println("Worker finished: " + myThreadId);
}
}
public static int getClusterStartupTime(Cache<String, Integer> cache, String cacheKey, EntryInfo wrapper) {
int startupTime = new Random().nextInt(1024);
// Concurrency doesn't work correctly with this
//Integer existingClusterStartTime = (Integer) cache.putIfAbsent(cacheKey, startupTime);
// Concurrency works fine with this
RemoteCache remoteCache = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next().getRemoteCache();
Integer existingClusterStartTime = (Integer) remoteCache.withFlags(Flag.FORCE_RETURN_VALUE).putIfAbsent(cacheKey, startupTime);
if (existingClusterStartTime == null) {
wrapper.successfulInitializations.incrementAndGet();
return startupTime;
} else {
return existingClusterStartTime;
}
}
private static class EntryInfo {
AtomicInteger successfulInitializations = new AtomicInteger(0);
AtomicInteger successfulListenerWrites = new AtomicInteger(0);
AtomicInteger th1 = new AtomicInteger();
AtomicInteger th2 = new AtomicInteger();
@Override
public String toString() {
return String.format("Inits: %d, listeners: %d, th1: %d, th2: %d", successfulInitializations.get(), successfulListenerWrites.get(), th1.get(), th2.get());
}
}
}

View file

@ -146,7 +146,8 @@ public class JpaRealmProvider implements RealmProvider {
query.setParameter("realm", realm.getId());
List<String> clients = query.getResultList();
for (String client : clients) {
session.realms().removeClient(client, adapter);
// No need to go through cache. Clients were already invalidated
removeClient(client, adapter);
}
for (ClientTemplateEntity a : new LinkedList<>(realm.getClientTemplates())) {
@ -154,7 +155,8 @@ public class JpaRealmProvider implements RealmProvider {
}
for (RoleModel role : adapter.getRoles()) {
session.realms().removeRole(adapter, role);
// No need to go through cache. Roles were already invalidated
removeRole(adapter, role);
}
@ -486,7 +488,8 @@ public class JpaRealmProvider implements RealmProvider {
session.users().preRemove(realm, client);
for (RoleModel role : client.getRoles()) {
client.removeRole(role);
// No need to go through cache. Roles were already invalidated
removeRole(realm, role);
}
ClientEntity clientEntity = ((ClientAdapter)client).getEntity();

View file

@ -952,13 +952,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
return session.realms().getRoleById(id, this);
}
@Override
public boolean removeRoleById(String id) {
RoleModel role = getRoleById(id);
if (role == null) return false;
return role.getContainer().removeRole(role);
}
@Override
public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) {
@ -1932,12 +1925,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
return session.realms().createGroup(this, id, name);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
session.realms().addTopLevelGroup(this, subGroup);
}
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
session.realms().moveGroup(this, group, toParent);

View file

@ -23,7 +23,6 @@ import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
@ -62,10 +61,6 @@ import org.keycloak.models.mongo.keycloak.entities.UserFederationProviderEntity;
import org.keycloak.models.utils.ComponentUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -515,13 +510,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return session.realms().removeRole(this, role);
}
@Override
public boolean removeRoleById(String id) {
RoleModel role = getRoleById(id);
if (role == null) return false;
return removeRole(role);
}
@Override
public Set<RoleModel> getRoles() {
DBObject query = new QueryBuilder()
@ -554,12 +542,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return session.realms().createGroup(this, id, name);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
session.realms().addTopLevelGroup(this, subGroup);
}
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
session.realms().moveGroup(this, group, toParent);

View file

@ -633,6 +633,11 @@
<artifactId>infinispan-core</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-cachestore-remote</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>

View file

@ -29,6 +29,6 @@ public interface ClusterListener {
*
* @param event value of notification (Object added into the cache)
*/
void run(ClusterEvent event);
void eventReceived(ClusterEvent event);
}

View file

@ -48,7 +48,8 @@ public interface ClusterProvider extends Provider {
/**
* Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed
* Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed.
* When using {@link #ALL} as the taskKey, then listener will be always triggered for any value put into the cache.
*
* @param taskKey
* @param task
@ -57,10 +58,18 @@ public interface ClusterProvider extends Provider {
/**
* Notify registered listeners on all cluster nodes
* Notify registered listeners on all cluster nodes. It will notify listeners registered under given taskKey AND also listeners registered with {@link #ALL} key (those are always executed)
*
* @param taskKey
* @param event
* @param ignoreSender if true, then sender node itself won't receive the notification
*/
void notify(String taskKey, ClusterEvent event);
void notify(String taskKey, ClusterEvent event, boolean ignoreSender);
/**
* Special value to be used with {@link #registerListener} to specify that particular listener will be always triggered for all notifications
* with any key.
*/
String ALL = "ALL";
}

View file

@ -27,12 +27,12 @@ public interface CacheRealmProvider extends RealmProvider {
void clear();
RealmProvider getDelegate();
void registerRealmInvalidation(String id);
void registerRealmInvalidation(String id, String name);
void registerClientInvalidation(String id);
void registerClientInvalidation(String id, String clientId, String realmId);
void registerClientTemplateInvalidation(String id);
void registerRoleInvalidation(String id);
void registerRoleInvalidation(String id, String roleName, String roleContainerId);
void registerGroupInvalidation(String id);
}

View file

@ -351,8 +351,6 @@ public interface RealmModel extends RoleContainerModel {
void setNotBefore(int notBefore);
boolean removeRoleById(String id);
boolean isEventsEnabled();
void setEventsEnabled(boolean enabled);
@ -397,13 +395,6 @@ public interface RealmModel extends RoleContainerModel {
GroupModel createGroup(String name);
GroupModel createGroup(String id, String name);
/**
* Move Group to top realm level. Basically just sets group parent to null. You need to call this though
* to make sure caches are set properly
*
* @param subGroup
*/
void addTopLevelGroup(GroupModel subGroup);
GroupModel getGroupById(String id);
List<GroupModel> getGroups();
List<GroupModel> getTopLevelGroups();

View file

@ -172,7 +172,7 @@ public class UserStorageSyncManager {
}
UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider);
session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event);
session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event, false);
}
@ -282,7 +282,7 @@ public class UserStorageSyncManager {
}
@Override
public void run(ClusterEvent event) {
public void eventReceived(ClusterEvent event) {
final UserStorageProviderClusterEvent fedEvent = (UserStorageProviderClusterEvent) event;
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {

View file

@ -155,7 +155,7 @@ public class UsersSyncManager {
// Ensure all cluster nodes are notified
public void notifyToRefreshPeriodicSync(KeycloakSession session, RealmModel realm, UserFederationProviderModel federationProvider, boolean removed) {
FederationProviderClusterEvent event = FederationProviderClusterEvent.createEvent(removed, realm.getId(), federationProvider);
session.getProvider(ClusterProvider.class).notify(FEDERATION_TASK_KEY, event);
session.getProvider(ClusterProvider.class).notify(FEDERATION_TASK_KEY, event, false);
}
@ -265,7 +265,7 @@ public class UsersSyncManager {
}
@Override
public void run(ClusterEvent event) {
public void eventReceived(ClusterEvent event) {
final FederationProviderClusterEvent fedEvent = (FederationProviderClusterEvent) event;
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {

View file

@ -51,6 +51,7 @@ import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.services.scheduled.ClearExpiredEvents;
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
import org.keycloak.services.scheduled.ScheduledTaskRunner;
import org.keycloak.services.util.JsonConfigProvider;
import org.keycloak.services.util.ObjectMapperResolver;
import org.keycloak.timer.TimerProvider;
@ -321,7 +322,7 @@ public class KeycloakApplication extends Application {
try {
TimerProvider timer = session.getProvider(TimerProvider.class);
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions(), interval), interval, "ClearExpiredUserSessions");
timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions");
new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer);
new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer);
} finally {

View file

@ -232,6 +232,10 @@
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-cachestore-remote</artifactId>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>

View file

@ -238,7 +238,7 @@ public class LDAPGroupMapperSyncTest {
GroupModel model1 = realm.createGroup("model1");
realm.moveGroup(model1, null);
GroupModel model2 = realm.createGroup("model2");
kcGroup1.addChild(model2);
realm.moveGroup(model2, kcGroup1);
// Sync groups again from LDAP. Nothing deleted
syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);

View file

@ -0,0 +1,420 @@
/*
* 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.testsuite.model;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.infinispan.Cache;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.jboss.logging.Logger;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.util.cli.TestCacheUtils;
/**
* Requires execution with cluster (or external JDG) enabled and real database, which will be shared for both cluster nodes. Everything set by system properties:
*
* 1) Use those system properties to run against shared MySQL:
*
* -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver -Dkeycloak.connectionsJpa.user=keycloak
* -Dkeycloak.connectionsJpa.password=keycloak
*
*
* 2) Then either choose from:
*
* 2.a) Run test with 2 keycloak nodes in cluster. Add this system property for that: -Dkeycloak.connectionsInfinispan.clustered=true
*
* 2.b) Run test with 2 keycloak nodes without cluster, but instead with external JDG. Both keycloak servers will send invalidation events to the JDG server and receive the events from this JDG server.
* They don't communicate with each other. So JDG is man-in-the-middle.
*
* This assumes that you have JDG 7.0 server running on localhost with HotRod endpoint on port 11222 (which is default port anyway).
*
* You also need to have this cache configured in JDG_HOME/standalone/configuration/standalone.xml to infinispan subsystem :
*
* <local-cache name="work" start="EAGER" batching="false" />
*
* Finally, add this system property when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@Ignore
public class ClusterInvalidationTest {
protected static final Logger logger = Logger.getLogger(ClusterInvalidationTest.class);
private static final String REALM_NAME = "test";
private static final int SLEEP_TIME_MS = Integer.parseInt(System.getProperty("sleep.time", "500"));
private static TestListener listener1realms;
private static TestListener listener1users;
private static TestListener listener2realms;
private static TestListener listener2users;
@ClassRule
public static KeycloakRule server1 = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
InfinispanConnectionProvider infinispan = manager.getSession().getProvider(InfinispanConnectionProvider.class);
Cache cache = infinispan.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
listener1realms = new TestListener("server1 - realms", cache);
cache.addListener(listener1realms);
cache = infinispan.getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
listener1users = new TestListener("server1 - users", cache);
cache.addListener(listener1users);
}
});
@ClassRule
public static KeycloakRule server2 = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
InfinispanConnectionProvider infinispan = manager.getSession().getProvider(InfinispanConnectionProvider.class);
Cache cache = infinispan.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
listener2realms = new TestListener("server2 - realms", cache);
cache.addListener(listener2realms);
cache = infinispan.getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
listener2users = new TestListener("server2 - users", cache);
cache.addListener(listener2users);
}
}) {
@Override
protected void configureServer(KeycloakServer server) {
server.getConfig().setPort(8082);
}
@Override
protected void importRealm() {
}
@Override
protected void removeTestRealms() {
}
};
private static void clearListeners() {
listener1realms.getInvalidationsAndClear();
listener1users.getInvalidationsAndClear();
listener2realms.getInvalidationsAndClear();
listener2users.getInvalidationsAndClear();
}
@Test
public void testClusterInvalidation() throws Exception {
cacheEverything();
clearListeners();
KeycloakSession session1 = server1.startSession();
logger.info("UPDATE REALM");
RealmModel realm = session1.realms().getRealmByName(REALM_NAME);
realm.setDisplayName("foo");
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 3, realm.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 3, realm.getId());
// CREATES
logger.info("CREATE ROLE");
realm = session1.realms().getRealmByName(REALM_NAME);
realm.addRole("foo-role");
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.roles");
assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.roles");
logger.info("CREATE CLIENT");
realm = session1.realms().getRealmByName(REALM_NAME);
realm.addClient("foo-client");
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.realm.clients");
assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.realm.clients");
logger.info("CREATE GROUP");
realm = session1.realms().getRealmByName(REALM_NAME);
GroupModel group = realm.createGroup("foo-group");
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.top.groups");
assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.top.groups");
logger.info("CREATE CLIENT TEMPLATE");
realm = session1.realms().getRealmByName(REALM_NAME);
realm.addClientTemplate("foo-template");
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, realm.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 0, 2); // realm not cached on server2 due to previous invalidation
// UPDATES
logger.info("UPDATE ROLE");
realm = session1.realms().getRealmByName(REALM_NAME);
ClientModel testApp = realm.getClientByClientId("test-app");
RoleModel role = session1.realms().getClientRole(realm, testApp, "customer-user");
role.setDescription("Foo");
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, role.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 3, role.getId());
logger.info("UPDATE GROUP");
realm = session1.realms().getRealmByName(REALM_NAME);
group = KeycloakModelUtils.findGroupByPath(realm, "/topGroup");
group.grantRole(role);
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, group.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, group.getId());
logger.info("UPDATE CLIENT");
realm = session1.realms().getRealmByName(REALM_NAME);
testApp = realm.getClientByClientId("test-app");
testApp.setDescription("foo");;
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, testApp.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 3, testApp.getId());
// Cache client template on server2
KeycloakSession session2 = server2.startSession();
realm = session2.realms().getRealmByName(REALM_NAME);
realm.getClientTemplates().get(0);
logger.info("UPDATE CLIENT TEMPLATE");
realm = session1.realms().getRealmByName(REALM_NAME);
ClientTemplateModel clientTemplate = realm.getClientTemplates().get(0);
clientTemplate.setDescription("bar");
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, clientTemplate.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, clientTemplate.getId());
// Nothing yet invalidated in user cache
assertInvalidations(listener1users.getInvalidationsAndClear(), 0, 0);
assertInvalidations(listener2users.getInvalidationsAndClear(), 0, 0);
logger.info("UPDATE USER");
realm = session1.realms().getRealmByName(REALM_NAME);
UserModel user = session1.users().getUserByEmail("keycloak-user@localhost", realm);
user.setSingleAttribute("foo", "Bar");
session1 = commit(server1, session1, true);
assertInvalidations(listener1users.getInvalidationsAndClear(), 1, 5, user.getId(), "test.email.keycloak-user@localhost");
assertInvalidations(listener2users.getInvalidationsAndClear(), 1, 5, user.getId());
logger.info("UPDATE USER CONSENTS");
realm = session1.realms().getRealmByName(REALM_NAME);
testApp = realm.getClientByClientId("test-app");
user = session1.users().getUserByEmail("keycloak-user@localhost", realm);
session1.users().addConsent(realm, user.getId(), new UserConsentModel(testApp));
session1 = commit(server1, session1, true);
assertInvalidations(listener1users.getInvalidationsAndClear(), 1, 1, user.getId() + ".consents");
assertInvalidations(listener2users.getInvalidationsAndClear(), 1, 1, user.getId() + ".consents");
// REMOVALS
logger.info("REMOVE USER");
realm = session1.realms().getRealmByName(REALM_NAME);
user = session1.users().getUserByUsername("john-doh@localhost", realm);
session1.users().removeUser(realm, user);
session1 = commit(server1, session1, true);
assertInvalidations(listener1users.getInvalidationsAndClear(), 3, 5, user.getId(), user.getId() + ".consents", "test.username.john-doh@localhost");
assertInvalidations(listener2users.getInvalidationsAndClear(), 2, 5, user.getId(), user.getId() + ".consents");
cacheEverything();
logger.info("REMOVE CLIENT TEMPLATE");
realm = session1.realms().getRealmByName(REALM_NAME);
realm.removeClientTemplate(clientTemplate.getId());
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 5, realm.getId(), clientTemplate.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 5, realm.getId(), clientTemplate.getId());
cacheEverything();
logger.info("REMOVE ROLE");
realm = session1.realms().getRealmByName(REALM_NAME);
role = realm.getRole("user");
realm.removeRole(role);
ClientModel thirdparty = session1.realms().getClientByClientId("third-party", realm);
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 7, 10, role.getId(), realm.getId(), "test.roles", "test.user.roles", testApp.getId(), thirdparty.getId(), group.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 7, 10, role.getId(), realm.getId(), "test.roles", "test.user.roles", testApp.getId(), thirdparty.getId(), group.getId());
// all users invalidated
assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
cacheEverything();
logger.info("REMOVE GROUP");
realm = session1.realms().getRealmByName(REALM_NAME);
group = realm.getGroupById(group.getId());
String subgroupId = group.getSubGroups().iterator().next().getId();
realm.removeGroup(group);
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 3, 5, group.getId(), subgroupId, "test.top.groups");
assertInvalidations(listener2realms.getInvalidationsAndClear(), 3, 5, group.getId(), subgroupId, "test.top.groups");
// all users invalidated
assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
cacheEverything();
logger.info("REMOVE CLIENT");
realm = session1.realms().getRealmByName(REALM_NAME);
testApp = realm.getClientByClientId("test-app");
role = testApp.getRole("customer-user");
realm.removeClient(testApp.getId());
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 8, 12, testApp.getId(), testApp.getId() + ".roles", role.getId(), testApp.getId() + ".customer-user.roles", "test.realm.clients", thirdparty.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 8, 12, testApp.getId(), testApp.getId() + ".roles", role.getId(), testApp.getId() + ".customer-user.roles", "test.realm.clients", thirdparty.getId());
// all users invalidated
assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
cacheEverything();
logger.info("REMOVE REALM");
realm = session1.realms().getRealmByName(REALM_NAME);
session1.realms().removeRealm(realm.getId());
session1 = commit(server1, session1, true);
assertInvalidations(listener1realms.getInvalidationsAndClear(), 50, 200, realm.getId(), thirdparty.getId());
assertInvalidations(listener2realms.getInvalidationsAndClear(), 50, 200, realm.getId(), thirdparty.getId());
// all users invalidated
assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
//Thread.sleep(10000000);
}
private void assertInvalidations(Map<String, Object> invalidations, int low, int high, String... expectedNames) {
int size = invalidations.size();
Assert.assertTrue("Size was " + size + ". Entries were: " + invalidations.keySet(), size >= low);
Assert.assertTrue("Size was " + size + ". Entries were: " + invalidations.keySet(), size <= high);
for (String expected : expectedNames) {
Assert.assertTrue("Can't find " + expected + ". Entries were: " + invalidations.keySet(), invalidations.keySet().contains(expected));
}
}
private KeycloakSession commit(KeycloakRule rule, KeycloakSession session, boolean sleepAfterCommit) throws Exception {
session.getTransactionManager().commit();
session.close();
if (sleepAfterCommit) {
Thread.sleep(SLEEP_TIME_MS);
}
return rule.startSession();
}
private void cacheEverything() throws Exception {
KeycloakSession session1 = server1.startSession();
TestCacheUtils.cacheRealmWithEverything(session1, REALM_NAME);
session1 = commit(server1, session1, false);
KeycloakSession session2 = server2.startSession();
TestCacheUtils.cacheRealmWithEverything(session2, REALM_NAME);
session2 = commit(server1, session2, false);
}
@Listener(observation = Listener.Observation.PRE)
public static class TestListener {
private final String name;
private final Cache cache; // Just for debugging
private Map<String, Object> invalidations = new ConcurrentHashMap<>();
public TestListener(String name, Cache cache) {
this.name = name;
this.cache = cache;
}
@CacheEntryRemoved
public void cacheEntryRemoved(CacheEntryRemovedEvent event) {
logger.infof("%s: Invalidated %s: %s", name, event.getKey(), event.getValue());
invalidations.put(event.getKey().toString(), event.getValue());
}
Map<String, Object> getInvalidationsAndClear() {
Map<String, Object> newMap = new HashMap<>(invalidations);
invalidations.clear();
return newMap;
}
}
}

View file

@ -0,0 +1,115 @@
/*
* 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.testsuite.util.cli;
import java.util.Map;
import java.util.Set;
import org.infinispan.Cache;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CacheCommands {
public static class ListCachesCommand extends AbstractCommand {
@Override
public String getName() {
return "listCaches";
}
@Override
protected void doRunCommand(KeycloakSession session) {
InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
Set<String> cacheNames = ispnProvider.getCache("realms").getCacheManager().getCacheNames();
log.infof("Available caches: %s", cacheNames);
}
}
public static class GetCacheCommand extends AbstractCommand {
@Override
public String getName() {
return "getCache";
}
@Override
protected void doRunCommand(KeycloakSession session) {
String cacheName = getArg(0);
InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
Cache<Object, Object> cache = ispnProvider.getCache(cacheName);
if (cache == null) {
log.errorf("Cache '%s' doesn't exist", cacheName);
throw new HandledException();
}
printCache(cache);
}
private void printCache(Cache<Object, Object> cache) {
int size = cache.size();
log.infof("Cache %s, size: %d", cache.getName(), size);
if (size > 50) {
log.info("Skip printing cache recors due to big size");
} else {
for (Map.Entry<Object, Object> entry : cache.entrySet()) {
log.infof("%s=%s", entry.getKey(), entry.getValue());
}
}
}
@Override
public String printUsage() {
return super.printUsage() + " <cache-name> . cache-name is name of the infinispan cache provided by InfinispanConnectionProvider";
}
}
public static class CacheRealmObjectsCommand extends AbstractCommand {
@Override
public String getName() {
return "cacheRealmObjects";
}
@Override
protected void doRunCommand(KeycloakSession session) {
String realmName = getArg(0);
RealmModel realm = session.realms().getRealmByName(realmName);
if (realm == null) {
log.errorf("Realm not found: %s", realmName);
throw new HandledException();
}
TestCacheUtils.cacheRealmWithEverything(session, realmName);
}
@Override
public String printUsage() {
return super.printUsage() + " <realm-name>";
}
}
}

View file

@ -0,0 +1,127 @@
/*
* 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.testsuite.util.cli;
import java.util.HashSet;
import java.util.Set;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleCommands {
public static class CreateRoles extends AbstractCommand {
private String rolePrefix;
private String roleContainer;
@Override
public String getName() {
return "createRoles";
}
private class StateHolder {
int firstInThisBatch;
int countInThisBatch;
int remaining;
};
@Override
protected void doRunCommand(KeycloakSession session) {
rolePrefix = getArg(0);
roleContainer = getArg(1);
int first = getIntArg(2);
int count = getIntArg(3);
int batchCount = getIntArg(4);
final StateHolder state = new StateHolder();
state.firstInThisBatch = first;
state.remaining = count;
state.countInThisBatch = Math.min(batchCount, state.remaining);
while (state.remaining > 0) {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
createRolesInBatch(session, roleContainer, rolePrefix, state.firstInThisBatch, state.countInThisBatch);
}
});
// update state
state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch;
state.remaining = state.remaining - state.countInThisBatch;
state.countInThisBatch = Math.min(batchCount, state.remaining);
}
log.infof("Command finished. All roles from %s to %s created", rolePrefix + first, rolePrefix + (first + count - 1));
}
private void createRolesInBatch(KeycloakSession session, String roleContainer, String rolePrefix, int first, int count) {
RoleContainerModel container = getRoleContainer(session, roleContainer);
int last = first + count;
for (int counter = first; counter < last; counter++) {
String roleName = rolePrefix + counter;
RoleModel role = container.addRole(roleName);
}
log.infof("Roles from %s to %s created", rolePrefix + first, rolePrefix + (last - 1));
}
private RoleContainerModel getRoleContainer(KeycloakSession session, String roleContainer) {
String[] parts = roleContainer.split("/");
String realmName = parts[0];
RealmModel realm = session.realms().getRealmByName(realmName);
if (realm == null) {
log.errorf("Unknown realm: %s", realmName);
throw new HandledException();
}
if (parts.length == 1) {
return realm;
} else {
String clientId = parts[1];
ClientModel client = session.realms().getClientByClientId(clientId, realm);
if (client == null) {
log.errorf("Unknown client: %s", clientId);
throw new HandledException();
}
return client;
}
}
@Override
public String printUsage() {
return super.printUsage() + " <role-prefix> <role-container> <starting-role-offset> <total-count> <batch-size> . " +
"\n'total-count' refers to total count of newly created roles. 'batch-size' refers to number of created roles in each transaction. 'starting-role-offset' refers to starting role offset." +
"\nFor example if 'starting-role-offset' is 15 and total-count is 10 and role-prefix is 'test', it will create roles test15, test16, test17, ... , test24" +
"\n'role-container' is either realm (then use just realmName like 'demo' or client (then use realm/clientId like 'demo/my-client' .\n" +
"Example usage: " + super.printUsage() + " test demo 0 500 100";
}
}
}

View file

@ -0,0 +1,88 @@
/*
* 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.testsuite.util.cli;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class TestCacheUtils {
public static void cacheRealmWithEverything(KeycloakSession session, String realmName) {
RealmModel realm = session.realms().getRealmByName(realmName);
for (ClientModel client : realm.getClients()) {
realm.getClientById(client.getId());
realm.getClientByClientId(client.getClientId());
cacheRoles(session, realm, client);
}
cacheRoles(session, realm, realm);
for (GroupModel group : realm.getTopLevelGroups()) {
cacheGroupRecursive(realm, group);
}
for (ClientTemplateModel clientTemplate : realm.getClientTemplates()) {
realm.getClientTemplateById(clientTemplate.getId());
}
for (UserModel user : session.users().getUsers(realm)) {
session.users().getUserById(user.getId(), realm);
if (user.getEmail() != null) {
session.users().getUserByEmail(user.getEmail(), realm);
}
session.users().getUserByUsername(user.getUsername(), realm);
session.users().getConsents(realm, user.getId());
for (FederatedIdentityModel fedIdentity : session.users().getFederatedIdentities(user, realm)) {
session.users().getUserByFederatedIdentity(fedIdentity, realm);
}
}
}
private static void cacheRoles(KeycloakSession session, RealmModel realm, RoleContainerModel roleContainer) {
for (RoleModel role : roleContainer.getRoles()) {
realm.getRoleById(role.getId());
roleContainer.getRole(role.getName());
if (roleContainer instanceof RealmModel) {
session.realms().getRealmRole(realm, role.getName());
} else {
session.realms().getClientRole(realm, (ClientModel) roleContainer, role.getName());
}
}
}
private static void cacheGroupRecursive(RealmModel realm, GroupModel group) {
realm.getGroupById(group.getId());
for (GroupModel sub : group.getSubGroups()) {
cacheGroupRecursive(realm, sub);
}
}
}

View file

@ -57,7 +57,11 @@ public class TestsuiteCLI {
UserCommands.Remove.class,
UserCommands.Count.class,
UserCommands.GetUser.class,
SyncDummyFederationProviderCommand.class
SyncDummyFederationProviderCommand.class,
RoleCommands.CreateRoles.class,
CacheCommands.ListCachesCommand.class,
CacheCommands.GetCacheCommand.class,
CacheCommands.CacheRealmObjectsCommand.class
};
private final KeycloakSessionFactory sessionFactory;

View file

@ -97,7 +97,10 @@
"default": {
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
"async": "${keycloak.connectionsInfinispan.async:false}",
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}",
"remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
"remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
"remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
}
},

View file

@ -46,7 +46,8 @@ log4j.logger.org.keycloak.connections.jpa.updater.liquibase=${keycloak.liquibase
# log4j.logger.org.keycloak.models.sessions.infinispan.initializer=trace
# Enable to view cache activity
# log4j.logger.org.keycloak.models.cache=trace
#log4j.logger.org.keycloak.cluster.infinispan=trace
#log4j.logger.org.keycloak.models.cache.infinispan=debug
# Enable to view database updates
# log4j.logger.org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider=debug

View file

@ -92,10 +92,10 @@
<replacement placeholder="CACHE-CONTAINERS">
<cache-container name="keycloak" jndi-name="infinispan/Keycloak">
<transport lock-timeout="60000"/>
<invalidation-cache name="realms" mode="SYNC"/>
<invalidation-cache name="users" mode="SYNC">
<local-cache name="realms"/>
<local-cache name="users">
<eviction max-entries="10000" strategy="LRU"/>
</invalidation-cache>
</local-cache>
<distributed-cache name="sessions" mode="SYNC" owners="1"/>
<distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
<distributed-cache name="loginFailures" mode="SYNC" owners="1"/>