From a27be0cee7aac8977357dc56e6f1750a5e2b6960 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 4 Nov 2016 09:40:39 +0100 Subject: [PATCH] KEYCLOAK-3857 Clustered invalidation cache fixes and refactoring. Support for cross-DC for invalidation caches. --- .../keycloak-model-infinispan/main/module.xml | 3 + .../src/main/cli/keycloak-install-ha.cli | 6 +- misc/CrossDataCenter.md | 116 +++++ model/infinispan/pom.xml | 4 + .../infinispan/CrossDCAwareCacheFactory.java | 93 ++++ .../infinispan/InfinispanClusterProvider.java | 92 +--- .../InfinispanClusterProviderFactory.java | 105 +++-- .../InfinispanNotificationsManager.java | 204 +++++++++ .../KeycloakHotRodMarshallerFactory.java | 33 ++ .../infinispan/WrapperClusterEvent.java | 59 +++ ...ltInfinispanConnectionProviderFactory.java | 54 ++- .../models/cache/infinispan/CacheManager.java | 124 ++---- .../cache/infinispan/ClientAdapter.java | 12 +- .../InfinispanCacheRealmProviderFactory.java | 19 +- .../InfinispanUserCacheProviderFactory.java | 22 +- .../models/cache/infinispan/RealmAdapter.java | 30 +- .../cache/infinispan/RealmCacheManager.java | 139 ++---- .../cache/infinispan/RealmCacheSession.java | 198 ++++++--- .../models/cache/infinispan/RoleAdapter.java | 2 +- .../cache/infinispan/UserCacheManager.java | 68 ++- .../cache/infinispan/UserCacheSession.java | 115 ++--- .../infinispan/entities/CachedRealm.java | 9 +- .../entities/ClientTemplateQuery.java | 11 - .../{ => entities}/GroupListQuery.java | 4 +- .../infinispan/entities/RoleListQuery.java | 3 +- .../infinispan/events/ClientAddedEvent.java | 55 +++ .../infinispan/events/ClientRemovedEvent.java | 74 +++ .../events/ClientTemplateEvent.java | 52 +++ .../infinispan/events/ClientUpdatedEvent.java | 55 +++ .../infinispan/events/GroupAddedEvent.java | 54 +++ .../infinispan/events/GroupMovedEvent.java | 64 +++ .../infinispan/events/GroupRemovedEvent.java | 59 +++ .../infinispan/events/GroupUpdatedEvent.java | 52 +++ .../infinispan/events/InvalidationEvent.java | 43 ++ .../events/RealmCacheInvalidationEvent.java | 31 ++ .../infinispan/events/RealmRemovedEvent.java | 53 +++ .../infinispan/events/RealmUpdatedEvent.java | 53 +++ .../infinispan/events/RoleAddedEvent.java | 53 +++ .../infinispan/events/RoleRemovedEvent.java | 55 +++ .../infinispan/events/RoleUpdatedEvent.java | 55 +++ .../events/UserCacheInvalidationEvent.java | 31 ++ .../UserCacheRealmInvalidationEvent.java | 51 +++ .../events/UserConsentsUpdatedEvent.java | 51 +++ .../UserFederationLinkRemovedEvent.java | 72 +++ .../UserFederationLinkUpdatedEvent.java | 50 +++ .../events/UserFullInvalidationEvent.java | 78 ++++ .../infinispan/events/UserUpdatedEvent.java | 57 +++ .../stream/ClientQueryPredicate.java | 48 -- .../stream/ClientTemplateQueryPredicate.java | 40 -- .../stream/GroupQueryPredicate.java | 40 -- .../stream/RealmQueryPredicate.java | 40 -- .../infinispan/stream/RoleQueryPredicate.java | 40 -- .../InfinispanUserSessionInitializer.java | 13 +- .../ConcurrencyJDGRemoteCacheTest.java | 231 ++++++++++ .../keycloak/models/jpa/JpaRealmProvider.java | 9 +- .../org/keycloak/models/jpa/RealmAdapter.java | 13 - .../mongo/keycloak/adapters/RealmAdapter.java | 18 - pom.xml | 5 + .../org/keycloak/cluster/ClusterListener.java | 2 +- .../org/keycloak/cluster/ClusterProvider.java | 15 +- .../models/cache/CacheRealmProvider.java | 6 +- .../java/org/keycloak/models/RealmModel.java | 9 - .../managers/UserStorageSyncManager.java | 4 +- .../services/managers/UsersSyncManager.java | 4 +- .../resources/KeycloakApplication.java | 3 +- testsuite/integration/pom.xml | 4 + .../storage/ldap/LDAPGroupMapperSyncTest.java | 2 +- .../model/ClusterInvalidationTest.java | 420 ++++++++++++++++++ .../testsuite/util/cli/CacheCommands.java | 115 +++++ .../testsuite/util/cli/RoleCommands.java | 127 ++++++ .../testsuite/util/cli/TestCacheUtils.java | 88 ++++ .../testsuite/util/cli/TestsuiteCLI.java | 6 +- .../resources/META-INF/keycloak-server.json | 5 +- .../src/test/resources/log4j.properties | 3 +- .../keycloak-infinispan.xml | 6 +- 75 files changed, 3271 insertions(+), 768 deletions(-) create mode 100644 misc/CrossDataCenter.md create mode 100644 model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java create mode 100644 model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java create mode 100644 model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java create mode 100644 model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java delete mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java rename model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/{ => entities}/GroupListQuery.java (83%) create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java delete mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java delete mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java delete mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java delete mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java delete mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java create mode 100644 model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml index a80a008a32..e7fdb8abed 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml @@ -30,6 +30,9 @@ + + + diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli index 17fd5f0d25..a3b85f17c7 100644 --- a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli +++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli @@ -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") diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md new file mode 100644 index 0000000000..4146eaa9e6 --- /dev/null +++ b/misc/CrossDataCenter.md @@ -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" : + +``` + +``` + +- 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: + +``` + +``` + +3) Configure `work` cache to use remoteStore. You should use this: + +``` + + + true + org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory + + +``` + +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: + +``` + + + +``` + +5) Optional: Configure logging in standalone.xml to see what invalidation events were send: +```` + + + + + + +```` + +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 : + +``` + +``` diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml index f10fa60751..fba921c627 100755 --- a/model/infinispan/pom.xml +++ b/model/infinispan/pom.xml @@ -48,6 +48,10 @@ org.infinispan infinispan-core + + org.infinispan + infinispan-cachestore-remote + junit junit diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java new file mode 100644 index 0000000000..17795ca213 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java @@ -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 Marek Posolda + */ +abstract class CrossDCAwareCacheFactory { + + protected static final Logger logger = Logger.getLogger(CrossDCAwareCacheFactory.class); + + + abstract BasicCache getCache(); + + + static CrossDCAwareCacheFactory getFactory(Cache workCache, Set 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 workCache; + + InfinispanCacheWrapperFactory(Cache workCache) { + this.workCache = workCache; + } + + @Override + BasicCache getCache() { + return workCache; + } + + } + + + // We have external JDG configured. Cross-DC should be enabled + private static class RemoteCacheWrapperFactory extends CrossDCAwareCacheFactory { + + private final RemoteCache remoteCache; + + RemoteCacheWrapperFactory(RemoteCache remoteCache) { + this.remoteCache = remoteCache; + } + + @Override + BasicCache getCache() { + // Flags are per-invocation! + return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE); + } + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java index 8b77c25b6d..5a4bdb744b 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java @@ -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 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 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 cache) { - Transport transport = cache.getCacheManager().getTransport(); - return transport==null ? "local" : transport.getAddress().toString(); - } - - - private LockEntry createLockEntry(Cache 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; diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java index 75aef45327..a96621d7b2 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java @@ -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 Marek Posolda */ 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 workCache; - private Map 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 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 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 } - void registerListener(String taskKey, ClusterListener task) { - listeners.put(taskKey, task); - } - - @Listener - public class CacheEntryListener { - - @CacheEntryCreated - public void cacheEntryCreated(CacheEntryCreatedEvent event) { - if (!event.isPre()) { - trigger(event.getKey(), event.getValue()); - } - } - - @CacheEntryModified - public void cacheEntryModified(CacheEntryModifiedEvent 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); - } - } - } - } diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java new file mode 100644 index 0000000000..57cc003a53 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java @@ -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 Marek Posolda + */ +public class InfinispanNotificationsManager { + + protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class); + + private final MultivaluedHashMap listeners = new MultivaluedHashMap<>(); + + private final Cache workCache; + + private final String myAddress; + + + protected InfinispanNotificationsManager(Cache workCache, String myAddress) { + this.workCache = workCache; + this.myAddress = myAddress; + } + + + // Create and init manager including all listeners etc + public static InfinispanNotificationsManager create(Cache workCache, String myAddress, Set 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 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 event) { + eventReceived(event.getKey(), event.getValue()); + } + + @CacheEntryModified + public void cacheEntryModified(CacheEntryModifiedEvent event) { + eventReceived(event.getKey(), event.getValue()); + } + } + + + @ClientListener + public class HotRodListener { + + private final RemoteCache remoteCache; + + public HotRodListener(RemoteCache 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 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); + } + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java new file mode 100644 index 0000000000..4a73bf3fdf --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java @@ -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 Marek Posolda + */ +public class KeycloakHotRodMarshallerFactory { + + public static GenericJBossMarshaller getInstance() { + return new GenericJBossMarshaller(KeycloakHotRodMarshallerFactory.class.getClassLoader()); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java new file mode 100644 index 0000000000..b03dd70c0a --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java @@ -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 Marek Posolda + */ +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()); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 8ad75fd6a8..473aab9e9b 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -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); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java index ad1ba26fab..c254ea7164 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java @@ -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 revisions; protected final Cache cache; protected final UpdateCounter counter = new UpdateCounter(); @@ -63,9 +62,10 @@ public abstract class CacheManager { public CacheManager(Cache cache, Cache revisions) { this.cache = cache; this.revisions = revisions; - this.cache.addListener(this); } + protected abstract Logger getLogger(); + public Cache 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 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> predicate = getInvalidationPredicate(object); - if (predicate != null) runEvictions(predicate); - RealmCacheManager.logger.tracev("invalidating: {0}" + object.getClass().getName()); - } + public void sendInvalidationEvents(KeycloakSession session, Collection 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 event) { - if (!event.isPre()) - for (Map.Entry entry : event.getEntries().entrySet()) { - Object object = entry.getValue(); - bumpVersion(entry.getKey()); - if (object == null) continue; - RealmCacheManager.logger.tracev("evicting: {0}" + object.getClass().getName()); - Predicate> 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 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> current) { - Set 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 invalidations); - protected abstract Predicate> getInvalidationPredicate(Object object); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index 980957a04d..11c1c62ac7 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -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 diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java index 4bbe4c73db..c2ad8cef40 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java @@ -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 Bill Burke @@ -54,14 +54,23 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME); Cache 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"); } } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java index 14d420b778..e8c2ba14fb 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java @@ -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 Stian Thorgersen @@ -55,13 +55,25 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME); Cache 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"); } } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 069b34aec5..1748e3ca27 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -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); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java index 55e3b38777..b01dbabf31 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java @@ -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 Stian Thorgersen */ -@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 cache, Cache revisions) { super(cache, revisions); } - public void realmInvalidation(String id, Set invalidations) { - Predicate> predicate = getRealmInvalidationPredicate(id); - addInvalidations(predicate, invalidations); + public void realmUpdated(String id, String name, Set invalidations) { + invalidations.add(id); + invalidations.add(RealmCacheSession.getRealmByNameCacheKey(name)); } - public Predicate> getRealmInvalidationPredicate(String id) { - return RealmQueryPredicate.create().realm(id); + public void realmRemoval(String id, String name, Set invalidations) { + realmUpdated(id, name, invalidations); + + addInvalidations(InRealmPredicate.create().realm(id), invalidations); } - public void clientInvalidation(String id, Set invalidations) { - addInvalidations(getClientInvalidationPredicate(id), invalidations); + public void roleAdded(String roleContainerId, Set invalidations) { + invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId)); } - public Predicate> getClientInvalidationPredicate(String id) { - return ClientQueryPredicate.create().client(id); + public void roleUpdated(String roleContainerId, String roleName, Set invalidations) { + invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName)); } - public void roleInvalidation(String id, Set invalidations) { - addInvalidations(getRoleInvalidationPredicate(id), invalidations); + public void roleRemoval(String id, String roleName, String roleContainerId, Set invalidations) { + invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId)); + invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName)); + addInvalidations(HasRolePredicate.create().role(id), invalidations); } - public Predicate> getRoleInvalidationPredicate(String id) { - return HasRolePredicate.create().role(id); + public void groupQueriesInvalidations(String realmId, Set 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 invalidations) { - addInvalidations(getGroupInvalidationPredicate(id), invalidations); - + public void clientAdded(String realmId, String clientUUID, String clientId, Set invalidations) { + invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId)); } - public Predicate> getGroupInvalidationPredicate(String id) { - return GroupQueryPredicate.create().group(id); + public void clientUpdated(String realmId, String clientUuid, String clientId, Set invalidations) { + invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId)); } - public void clientTemplateInvalidation(String id, Set invalidations) { - addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations); + // Client roles invalidated separately + public void clientRemoval(String realmId, String clientUUID, String clientId, Set invalidations) { + invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId)); + invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId)); + addInvalidations(InClientPredicate.create().client(clientUUID), invalidations); } - public Predicate> getClientTemplateInvalidationPredicate(String id) { - return ClientTemplateQueryPredicate.create().template(id); - } - - public void realmRemoval(String id, Set invalidations) { - Predicate> predicate = getRealmRemovalPredicate(id); - addInvalidations(predicate, invalidations); - } - - public Predicate> getRealmRemovalPredicate(String id) { - Predicate> predicate = null; - predicate = RealmQueryPredicate.create().realm(id) - .or(InRealmPredicate.create().realm(id)); - return predicate; - } - - public void clientAdded(String realmId, String id, Set invalidations) { - addInvalidations(getClientAddedPredicate(realmId), invalidations); - } - - public Predicate> getClientAddedPredicate(String realmId) { - return ClientQueryPredicate.create().inRealm(realmId); - } - - public void clientRemoval(String realmId, String id, Set invalidations) { - Predicate> predicate = null; - predicate = getClientRemovalPredicate(realmId, id); - addInvalidations(predicate, invalidations); - } - - public Predicate> getClientRemovalPredicate(String realmId, String id) { - Predicate> predicate; - predicate = ClientQueryPredicate.create().inRealm(realmId) - .or(ClientQueryPredicate.create().client(id)) - .or(InClientPredicate.create().client(id)); - return predicate; - } - - public void roleRemoval(String id, Set invalidations) { - addInvalidations(getRoleRemovalPredicate(id), invalidations); - - } - - public Predicate> getRoleRemovalPredicate(String id) { - return getRoleInvalidationPredicate(id); - } @Override - protected Predicate> 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> 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 invalidations) { + if (event instanceof RealmCacheInvalidationEvent) { + invalidations.add(event.getId()); + + ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations); } - return null; } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 9321f47197..d61a611926 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.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 managedGroups = new HashMap<>(); protected Set listInvalidations = new HashSet<>(); protected Set invalidations = new HashSet<>(); + protected Set 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 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 diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java index a43aeb82f4..b6862f55e8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java @@ -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"); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java index ee8dc8b86a..e9493144e1 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java @@ -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 Stian Thorgersen */ -@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 cache, Cache 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 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 federatedIdentities, Set invalidations) { + userUpdatedInvalidations(userId, username, email, realmId, invalidations); + + if (identityFederationEnabled) { + // Invalidate all keys for lookup this user by any identityProvider link + for (Map.Entry 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 invalidations) { + invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId)); + } + + public void federatedIdentityLinkRemovedInvalidation(String userId, String realmId, String identityProviderId, String socialUserId, Set invalidations) { + invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId)); + if (identityProviderId != null) { + invalidations.add(UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, identityProviderId, socialUserId)); + } + } + + public void consentInvalidation(String userId, Set invalidations) { + invalidations.add(UserCacheSession.getConsentCacheKey(userId)); + } + + @Override - protected Predicate> getInvalidationPredicate(Object object) { - return null; + protected void addInvalidationsFromEvent(InvalidationEvent event, Set invalidations) { + if (event instanceof UserCacheInvalidationEvent) { + ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations); + } } public void invalidateRealmUsers(String realm, Set invalidations) { - addInvalidations(InRealmPredicate.create().realm(realm), invalidations); + InRealmPredicate inRealmPredicate = getInRealmPredicate(realm); + addInvalidations(inRealmPredicate, invalidations); + } + + private InRealmPredicate getInRealmPredicate(String realmId) { + return InRealmPredicate.create().realm(realmId); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 5531de1753..92d56a8cfd 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.models.cache.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 invalidations = new HashSet<>(); protected Set realmInvalidations = new HashSet<>(); + protected Set invalidationEvents = new HashSet<>(); // Events to be sent across cluster protected Map 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 federatedIdentities = realm.isIdentityFederationEnabled() ? getFederatedIdentities(user, realm) : null; - if (realm.isIdentityFederationEnabled()) { - // Invalidate all keys for lookup this user by any identityProvider link - Set 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)); + } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java index 4647f74f68..5dd4bacaf7 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java @@ -135,7 +135,6 @@ public class CachedRealm extends AbstractExtendableRevisioned { } protected List defaultGroups = new LinkedList(); - protected Set groups = new HashSet(); protected List clientTemplates= new LinkedList<>(); protected boolean internationalizationEnabled; protected Set 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 getGroups() { - return groups; - } - public List getDefaultGroups() { return defaultGroups; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java deleted file mode 100755 index a6fcebf836..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.keycloak.models.cache.infinispan.entities; - -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface ClientTemplateQuery extends InRealm { - Set getTemplates(); -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/GroupListQuery.java similarity index 83% rename from model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupListQuery.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/GroupListQuery.java index 1fa44115de..1e0c664e1e 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupListQuery.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/GroupListQuery.java @@ -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; diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java index 21a73adf6e..e924c0595b 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java @@ -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 + '\'' + '}'; } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java new file mode 100644 index 0000000000..1b022ca4cf --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.clientAdded(realmId, clientUuid, clientId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java new file mode 100644 index 0000000000..2e620db06e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java @@ -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 Marek Posolda + */ +public class ClientRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent { + + private String clientUuid; + private String clientId; + private String realmId; + // roleId -> roleName + private Map 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 invalidations) { + realmCache.clientRemoval(realmId, clientUuid, clientId, invalidations); + + // Separate iteration for all client roles to invalidate records dependent on them + for (Map.Entry clientRole : clientRoles.entrySet()) { + String roleId = clientRole.getKey(); + String roleName = clientRole.getValue(); + realmCache.roleRemoval(roleId, roleName, clientUuid, invalidations); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java new file mode 100644 index 0000000000..7bb13a9388 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + // Nothing. ID was already invalidated + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java new file mode 100644 index 0000000000..cc6c263fe0 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.clientUpdated(realmId, clientUuid, clientId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java new file mode 100644 index 0000000000..77dcf69ad2 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.groupQueriesInvalidations(realmId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java new file mode 100644 index 0000000000..2f5566aed3 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.groupQueriesInvalidations(realmId, invalidations); + if (newParentId != null) { + invalidations.add(newParentId); + } + if (oldParentId != null) { + invalidations.add(oldParentId); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java new file mode 100644 index 0000000000..37689faca5 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.groupQueriesInvalidations(realmId, invalidations); + if (parentId != null) { + invalidations.add(parentId); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java new file mode 100644 index 0000000000..c59021b446 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + // Nothing. ID already invalidated + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java new file mode 100644 index 0000000000..ea59ff5f1f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java @@ -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 Marek Posolda + */ +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; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java new file mode 100644 index 0000000000..2876e08e3d --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java @@ -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 Marek Posolda + */ +public interface RealmCacheInvalidationEvent { + + void addInvalidations(RealmCacheManager realmCache, Set invalidations); + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java new file mode 100644 index 0000000000..355875734f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.realmRemoval(realmId, realmName, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java new file mode 100644 index 0000000000..624fc6da93 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.realmUpdated(realmId, realmName, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java new file mode 100644 index 0000000000..cb393e5be8 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.roleAdded(containerId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java new file mode 100644 index 0000000000..6137b1bdae --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.roleRemoval(roleId, roleName, containerId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java new file mode 100644 index 0000000000..4b2ae5b2df --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.roleUpdated(containerId, roleName, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java new file mode 100644 index 0000000000..964e97ab43 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java @@ -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 Marek Posolda + */ +public interface UserCacheInvalidationEvent { + + void addInvalidations(UserCacheManager userCache, Set invalidations); + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java new file mode 100644 index 0000000000..39961815d4 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.invalidateRealmUsers(realmId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java new file mode 100644 index 0000000000..021e84180f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.consentInvalidation(userId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java new file mode 100644 index 0000000000..15704df27e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.federatedIdentityLinkRemovedInvalidation(userId, realmId, identityProviderId, socialUserId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java new file mode 100644 index 0000000000..8bbfb41210 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java new file mode 100644 index 0000000000..d637ac2f54 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java @@ -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 Marek Posolda + */ +public class UserFullInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent { + + private String userId; + private String username; + private String email; + private String realmId; + private boolean identityFederationEnabled; + private Map federatedIdentities; + + public static UserFullInvalidationEvent create(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Collection 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 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 invalidations) { + userCache.fullUserInvalidation(userId, username, email, realmId, identityFederationEnabled, federatedIdentities, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java new file mode 100644 index 0000000000..429b4af1dd --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.userUpdatedInvalidations(userId, username, email, realmId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java deleted file mode 100755 index bf4ade8156..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class ClientQueryPredicate implements Predicate>, 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 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; - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java deleted file mode 100755 index fba0c02ff6..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class ClientTemplateQueryPredicate implements Predicate>, 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 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); - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java deleted file mode 100755 index 855930e0d3..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class GroupQueryPredicate implements Predicate>, 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 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); - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java deleted file mode 100755 index dbb64f5b79..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class RealmQueryPredicate implements Predicate>, 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 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); - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java deleted file mode 100755 index 5e37d59e60..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class RoleQueryPredicate implements Predicate>, 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 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); - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java index 1485da837b..c332eea8fc 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java @@ -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); } diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java new file mode 100644 index 0000000000..e7c1337934 --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java @@ -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 Marek Posolda + */ +@Ignore +public class ConcurrencyJDGRemoteCacheTest { + + private static Map 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 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 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 cache; + + private final int myThreadId; + + private Worker(Cache cache, int myThreadId) { + this.cache = cache; + this.myThreadId = myThreadId; + } + + @Override + public void run() { + for (Map.Entry 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 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()); + } + } + + + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index f5d266602d..55e4108675 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -146,7 +146,8 @@ public class JpaRealmProvider implements RealmProvider { query.setParameter("realm", realm.getId()); List 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(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 97aa4bd55d..f5b9d7df51 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -952,13 +952,6 @@ public class RealmAdapter implements RealmModel, JpaModel { 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 { 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); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index a79c478bc7..bd00f9dcfa 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -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 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 getRoles() { DBObject query = new QueryBuilder() @@ -554,12 +542,6 @@ public class RealmAdapter extends AbstractMongoAdapter 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); diff --git a/pom.xml b/pom.xml index bfa291ac52..afd4212a9f 100755 --- a/pom.xml +++ b/pom.xml @@ -633,6 +633,11 @@ infinispan-core ${infinispan.version} + + org.infinispan + infinispan-cachestore-remote + ${infinispan.version} + org.liquibase liquibase-core diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java index 41c65c03d5..2c07377e36 100644 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java +++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java @@ -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); } diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java index 6c22056c99..abed174520 100644 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java @@ -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"; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java index 7bc1299764..61ae1beabc 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java @@ -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); } diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index e9df047418..09720a7b5f 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -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 getGroups(); List getTopLevelGroups(); diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java index 05b07f2ed4..b1114fabe2 100755 --- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java @@ -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() { diff --git a/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java index d8c5dee5ad..21b0cad243 100755 --- a/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java @@ -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() { diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 363d6f4774..2ea9992fd2 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -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 { diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 873d99ddfe..8a2dc4b29f 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -232,6 +232,10 @@ org.infinispan infinispan-core + + org.infinispan + infinispan-cachestore-remote + org.seleniumhq.selenium selenium-java diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java index 66321245d3..1fec6d9f01 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java @@ -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); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java new file mode 100644 index 0000000000..f71d0da25a --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java @@ -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 : + * + * + * + * Finally, add this system property when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true + * + * @author Marek Posolda + */ +@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 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 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 getInvalidationsAndClear() { + Map newMap = new HashMap<>(invalidations); + invalidations.clear(); + return newMap; + } + + } + + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java new file mode 100644 index 0000000000..0c7eff0450 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java @@ -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 Marek Posolda + */ +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 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 cache = ispnProvider.getCache(cacheName); + if (cache == null) { + log.errorf("Cache '%s' doesn't exist", cacheName); + throw new HandledException(); + } + + printCache(cache); + } + + private void printCache(Cache 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 entry : cache.entrySet()) { + log.infof("%s=%s", entry.getKey(), entry.getValue()); + } + } + } + + @Override + public String printUsage() { + return super.printUsage() + " . 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() + " "; + } + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java new file mode 100644 index 0000000000..2c71c72116 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java @@ -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 Marek Posolda + */ +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() + " . " + + "\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"; + } + + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java new file mode 100644 index 0000000000..9792f9dff6 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java @@ -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 Marek Posolda + */ +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); + } + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java index 8e9582bfd0..9b2c17aaca 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java @@ -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; diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index 06b4e5266f..3f4ddd1a1e 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -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}" } }, diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties index f0ff6ac6e3..2fa1d70fbc 100755 --- a/testsuite/integration/src/test/resources/log4j.properties +++ b/testsuite/integration/src/test/resources/log4j.properties @@ -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 diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml index 840fa2ca24..3c8f429e1d 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml @@ -92,10 +92,10 @@ - - + + - +