keycloak-uncached
Merge pull request #4391 from mposolda/ispn-clientListeners-bugs KEYCLOAK-4634 …
Changes
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 9(+8 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java 65(+35 -30)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/LoginFailuresUpdateTask.java 36(+36 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java 2(+1 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java 2(+1 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java 2(+1 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java 2(+0 -2)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java 33(+33 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java 39(+27 -12)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java 5(+5 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java 32(+5 -27)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java 27(+27 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 97(+71 -26)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java 7(+6 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java 23(+13 -10)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java 28(+14 -14)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java 4(+2 -2)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java 7(+4 -3)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java 70(+54 -16)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java 44(+17 -27)
Details
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 86f6074..a17e724 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
@@ -262,7 +262,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
sessionCacheConfiguration = sessionConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
- cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfigurationBase);
+ if (jdgEnabled) {
+ sessionConfigBuilder = new ConfigurationBuilder();
+ sessionConfigBuilder.read(sessionCacheConfigurationBase);
+ configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class);
+ }
+ sessionCacheConfiguration = sessionConfigBuilder.build();
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
+
cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfigurationBase);
// Retrieve caches to enforce rebalance
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
index 43bb2b3..e5a7a47 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
@@ -33,18 +33,18 @@ import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extends AbstractKeycloakTransaction {
+public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> extends AbstractKeycloakTransaction {
public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class);
private final KeycloakSession kcSession;
private final String cacheName;
- private final Cache<String, SessionEntityWrapper<S>> cache;
+ private final Cache<K, SessionEntityWrapper<V>> cache;
private final RemoteCacheInvoker remoteCacheInvoker;
- private final Map<String, SessionUpdatesList<S>> updates = new HashMap<>();
+ private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
- public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache<String, SessionEntityWrapper<S>> cache, RemoteCacheInvoker remoteCacheInvoker) {
+ public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker) {
this.kcSession = kcSession;
this.cacheName = cacheName;
this.cache = cache;
@@ -52,11 +52,11 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
}
- public void addTask(String key, SessionUpdateTask<S> task) {
- SessionUpdatesList<S> myUpdates = updates.get(key);
+ public void addTask(K key, SessionUpdateTask<V> task) {
+ SessionUpdatesList<V> myUpdates = updates.get(key);
if (myUpdates == null) {
// Lookup entity from cache
- SessionEntityWrapper<S> wrappedEntity = cache.get(key);
+ SessionEntityWrapper<V> wrappedEntity = cache.get(key);
if (wrappedEntity == null) {
logger.warnf("Not present cache item for key %s", key);
return;
@@ -75,14 +75,14 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
// Create entity and new version for it
- public void addTask(String key, SessionUpdateTask<S> task, S entity) {
+ public void addTask(K key, SessionUpdateTask<V> task, V entity) {
if (entity == null) {
throw new IllegalArgumentException("Null entity not allowed");
}
RealmModel realm = kcSession.realms().getRealm(entity.getRealm());
- SessionEntityWrapper<S> wrappedEntity = new SessionEntityWrapper<>(entity);
- SessionUpdatesList<S> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
+ SessionEntityWrapper<V> wrappedEntity = new SessionEntityWrapper<>(entity);
+ SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
updates.put(key, myUpdates);
// Run the update now, so reader in same transaction can see it
@@ -91,19 +91,19 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
}
- public void reloadEntityInCurrentTransaction(RealmModel realm, String key, SessionEntityWrapper<S> entity) {
+ public void reloadEntityInCurrentTransaction(RealmModel realm, K key, SessionEntityWrapper<V> entity) {
if (entity == null) {
throw new IllegalArgumentException("Null entity not allowed");
}
- SessionEntityWrapper<S> latestEntity = cache.get(key);
+ SessionEntityWrapper<V> latestEntity = cache.get(key);
if (latestEntity == null) {
return;
}
- SessionUpdatesList<S> newUpdates = new SessionUpdatesList<>(realm, latestEntity);
+ SessionUpdatesList<V> newUpdates = new SessionUpdatesList<>(realm, latestEntity);
- SessionUpdatesList<S> existingUpdates = updates.get(key);
+ SessionUpdatesList<V> existingUpdates = updates.get(key);
if (existingUpdates != null) {
newUpdates.setUpdateTasks(existingUpdates.getUpdateTasks());
}
@@ -112,10 +112,10 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
}
- public SessionEntityWrapper<S> get(String key) {
- SessionUpdatesList<S> myUpdates = updates.get(key);
+ public SessionEntityWrapper<V> get(K key) {
+ SessionUpdatesList<V> myUpdates = updates.get(key);
if (myUpdates == null) {
- SessionEntityWrapper<S> wrappedEntity = cache.get(key);
+ SessionEntityWrapper<V> wrappedEntity = cache.get(key);
if (wrappedEntity == null) {
return null;
}
@@ -127,7 +127,7 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
return wrappedEntity;
} else {
- S entity = myUpdates.getEntityWrapper().getEntity();
+ V entity = myUpdates.getEntityWrapper().getEntity();
// If entity is scheduled for remove, we don't return it.
boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> {
@@ -143,13 +143,13 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
@Override
protected void commitImpl() {
- for (Map.Entry<String, SessionUpdatesList<S>> entry : updates.entrySet()) {
- SessionUpdatesList<S> sessionUpdates = entry.getValue();
- SessionEntityWrapper<S> sessionWrapper = sessionUpdates.getEntityWrapper();
+ for (Map.Entry<K, SessionUpdatesList<V>> entry : updates.entrySet()) {
+ SessionUpdatesList<V> sessionUpdates = entry.getValue();
+ SessionEntityWrapper<V> sessionWrapper = sessionUpdates.getEntityWrapper();
RealmModel realm = sessionUpdates.getRealm();
- MergedUpdate<S> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper);
+ MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper);
if (merged != null) {
// Now run the operation in our cluster
@@ -162,8 +162,8 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
}
- private void runOperationInCluster(String key, MergedUpdate<S> task, SessionEntityWrapper<S> sessionWrapper) {
- S session = sessionWrapper.getEntity();
+ private void runOperationInCluster(K key, MergedUpdate<V> task, SessionEntityWrapper<V> sessionWrapper) {
+ V session = sessionWrapper.getEntity();
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
// Don't need to run update of underlying entity. Local updates were already run
@@ -182,9 +182,14 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
.put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS);
break;
case ADD_IF_ABSENT:
- SessionEntityWrapper existing = cache.putIfAbsent(key, sessionWrapper);
+ SessionEntityWrapper<V> existing = cache.putIfAbsent(key, sessionWrapper);
if (existing != null) {
- throw new IllegalStateException("There is already existing value in cache for key " + key);
+ logger.debugf("Existing entity in cache for key: %s . Will update it", key);
+
+ // Apply updates on the existing entity and replace it
+ task.runUpdate(existing.getEntity());
+
+ replace(key, task, existing);
}
break;
case REPLACE:
@@ -197,12 +202,12 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
}
- private void replace(String key, MergedUpdate<S> task, SessionEntityWrapper<S> oldVersionEntity) {
+ private void replace(K key, MergedUpdate<V> task, SessionEntityWrapper<V> oldVersionEntity) {
boolean replaced = false;
- S session = oldVersionEntity.getEntity();
+ V session = oldVersionEntity.getEntity();
while (!replaced) {
- SessionEntityWrapper<S> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
+ SessionEntityWrapper<V> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
// Atomic cluster-aware replace
replaced = cache.replace(key, oldVersionEntity, newVersionEntity);
@@ -235,7 +240,7 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
protected void rollbackImpl() {
}
- private SessionEntityWrapper<S> generateNewVersionAndWrapEntity(S entity, Map<String, String> localMetadata) {
+ private SessionEntityWrapper<V> generateNewVersionAndWrapEntity(V entity, Map<String, String> localMetadata) {
return new SessionEntityWrapper<>(localMetadata, entity);
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/LoginFailuresUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/LoginFailuresUpdateTask.java
new file mode 100644
index 0000000..04ed05a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/LoginFailuresUpdateTask.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 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.sessions.infinispan.changes;
+
+import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class LoginFailuresUpdateTask implements SessionUpdateTask<LoginFailureEntity> {
+
+ @Override
+ public CacheOperation getOperation(LoginFailureEntity session) {
+ return CacheOperation.REPLACE;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<LoginFailureEntity> sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
index 1f24f84..8e0cf0e 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
@@ -32,7 +32,7 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
private CrossDCMessageStatus crossDCMessageStatus;
- public MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) {
+ private MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) {
this.operation = operation;
this.crossDCMessageStatus = crossDCMessageStatus;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
index f9adf9b..25e0df9 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
@@ -60,7 +60,7 @@ public class LastSessionRefreshChecker {
Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE);
if (lsrr == null) {
- logger.warnf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId());
+ logger.debugf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId());
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java
index b79ea16..244a88b 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java
@@ -49,7 +49,7 @@ public interface SessionUpdateTask<S extends SessionEntity> {
if (this == ADD | this == ADD_IF_ABSENT) {
if (other == ADD | other == ADD_IF_ABSENT) {
- throw new IllegalStateException("Illegal state. Task already in progress for session " + entity.getId());
+ throw new IllegalStateException("Illegal state. Task already in progress for session " + entity.toString());
}
return this;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java
index 4fd4bbe..1db36a1 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java
@@ -17,8 +17,6 @@
package org.keycloak.models.sessions.infinispan.changes;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
index 0b99225..cdb841e 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import org.keycloak.sessions.AuthenticationSessionModel;
@@ -29,6 +30,8 @@ import org.keycloak.sessions.AuthenticationSessionModel;
*/
public class AuthenticationSessionEntity extends SessionEntity {
+ private String id;
+
private String clientUuid;
private String authUserId;
@@ -46,6 +49,14 @@ public class AuthenticationSessionEntity extends SessionEntity {
private Set<String> requiredActions = new HashSet<>();
private Map<String, String> userSessionNotes;
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
public String getClientUuid() {
return clientUuid;
}
@@ -149,4 +160,26 @@ public class AuthenticationSessionEntity extends SessionEntity {
public void setAuthNotes(Map<String, String> authNotes) {
this.authNotes = authNotes;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AuthenticationSessionEntity)) return false;
+
+ AuthenticationSessionEntity that = (AuthenticationSessionEntity) o;
+
+ if (id != null ? !id.equals(that.id) : that.id != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return id != null ? id.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AuthenticationSessionEntity [id=%s, realm=%s, clientUuid=%s ]", getId(), getRealm(), getClientUuid());
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java
index d25f58b..5b328ec 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java
@@ -17,15 +17,12 @@
package org.keycloak.models.sessions.infinispan.entities;
-import java.io.Serializable;
-
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class LoginFailureEntity implements Serializable {
+public class LoginFailureEntity extends SessionEntity {
private String userId;
- private String realm;
private int failedLoginNotBefore;
private int numFailures;
private long lastFailure;
@@ -39,14 +36,6 @@ public class LoginFailureEntity implements Serializable {
this.userId = userId;
}
- public String getRealm() {
- return realm;
- }
-
- public void setRealm(String realm) {
- this.realm = realm;
- }
-
public int getFailedLoginNotBefore() {
return failedLoginNotBefore;
}
@@ -85,4 +74,30 @@ public class LoginFailureEntity implements Serializable {
this.lastFailure = 0;
this.lastIPFailure = null;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof LoginFailureEntity)) return false;
+
+ LoginFailureEntity that = (LoginFailureEntity) o;
+
+ if (userId != null ? !userId.equals(that.userId) : that.userId != null) return false;
+ if (getRealm() != null ? !getRealm().equals(that.getRealm()) : that.getRealm() != null) return false;
+
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = getRealm() != null ? getRealm().hashCode() : 0;
+ hashCode = hashCode * 13 + (userId != null ? userId.hashCode() : 0);
+ return hashCode;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("LoginFailureEntity [ userId=%s, realm=%s, numFailures=%d ]", userId, getRealm(), numFailures);
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java
index c452379..318b1ba 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java
@@ -52,4 +52,9 @@ public class LoginFailureKey implements Serializable {
return result;
}
+
+ @Override
+ public String toString() {
+ return String.format("LoginFailureKey [ realm=%s. userId=%s ]", realm, userId);
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
index 25ac2a4..37f8e08 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
@@ -26,17 +26,8 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
*/
public abstract class SessionEntity implements Serializable {
- private String id;
-
private String realm;
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
public String getRealm() {
return realm;
@@ -46,26 +37,13 @@ public abstract class SessionEntity implements Serializable {
this.realm = realm;
}
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof SessionEntity)) return false;
-
- SessionEntity that = (SessionEntity) o;
-
- if (id != null ? !id.equals(that.id) : that.id != null) return false;
-
- return true;
- }
-
- @Override
- public int hashCode() {
- return id != null ? id.hashCode() : 0;
- }
-
public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
- throw new IllegalStateException("Not yet implemented");
+ if (localEntityWrapper == null) {
+ return new SessionEntityWrapper<>(this);
+ } else {
+ return new SessionEntityWrapper<>(localEntityWrapper.getLocalMetadata(), this);
+ }
};
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
index 5d0edb0..8ac85a1 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
@@ -43,6 +43,8 @@ public class UserSessionEntity extends SessionEntity {
// Metadata attribute, which contains the lastSessionRefresh available on remoteCache. Used in decide whether we need to write to remoteCache (DC) or not
public static final String LAST_SESSION_REFRESH_REMOTE = "lsrr";
+ private String id;
+
private String user;
private String brokerSessionId;
@@ -62,6 +64,14 @@ public class UserSessionEntity extends SessionEntity {
private UserSessionModel.State state;
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
private Map<String, String> notes = new ConcurrentHashMap<>();
private Map<String, AuthenticatedClientSessionEntity> authenticatedClientSessions = new ConcurrentHashMap<>();
@@ -163,6 +173,23 @@ public class UserSessionEntity extends SessionEntity {
}
@Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof UserSessionEntity)) return false;
+
+ UserSessionEntity that = (UserSessionEntity) o;
+
+ if (id != null ? !id.equals(that.id) : that.id != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return id != null ? id.hashCode() : 0;
+ }
+
+ @Override
public String toString() {
return String.format("UserSessionEntity [id=%s, realm=%s, lastSessionRefresh=%d, clients=%s]", getId(), getRealm(), getLastSessionRefresh(),
new TreeSet(this.authenticatedClientSessions.keySet()));
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 1eb58b5..48d1965 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -42,7 +42,6 @@ import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessi
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
-import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
@@ -76,11 +75,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache;
protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache;
- protected final Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache;
+ protected final Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache;
- protected final InfinispanChangelogBasedTransaction<UserSessionEntity> sessionTx;
- protected final InfinispanChangelogBasedTransaction<UserSessionEntity> offlineSessionTx;
- protected final InfinispanKeycloakTransaction tx;
+ protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx;
+ protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx;
+ protected final InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> loginFailuresTx;
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
@@ -93,7 +92,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
LastSessionRefreshStore offlineLastSessionRefreshStore,
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache,
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
- Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache) {
+ Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache) {
this.session = session;
this.sessionCache = sessionCache;
@@ -103,24 +102,24 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCache, remoteCacheInvoker);
this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionCache, remoteCacheInvoker);
- this.tx = new InfinispanKeycloakTransaction();
+ this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, loginFailureCache, remoteCacheInvoker);
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
this.lastSessionRefreshStore = lastSessionRefreshStore;
this.offlineLastSessionRefreshStore = offlineLastSessionRefreshStore;
- session.getTransactionManager().enlistAfterCompletion(tx);
session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
session.getTransactionManager().enlistAfterCompletion(sessionTx);
session.getTransactionManager().enlistAfterCompletion(offlineSessionTx);
+ session.getTransactionManager().enlistAfterCompletion(loginFailuresTx);
}
protected Cache<String, SessionEntityWrapper<UserSessionEntity>> getCache(boolean offline) {
return offline ? offlineSessionCache : sessionCache;
}
- protected InfinispanChangelogBasedTransaction<UserSessionEntity> getTransaction(boolean offline) {
+ protected InfinispanChangelogBasedTransaction<String, UserSessionEntity> getTransaction(boolean offline) {
return offline ? offlineSessionTx : sessionTx;
}
@@ -136,7 +135,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
- InfinispanChangelogBasedTransaction<UserSessionEntity> updateTx = getTransaction(false);
+ InfinispanChangelogBasedTransaction<String, UserSessionEntity> updateTx = getTransaction(false);
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, updateTx);
adapter.setUserSession(userSession);
return adapter;
@@ -202,7 +201,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
private UserSessionEntity getUserSessionEntity(String id, boolean offline) {
- InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+ InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
SessionEntityWrapper<UserSessionEntity> entityWrapper = tx.get(id);
return entityWrapper==null ? null : entityWrapper.getEntity();
}
@@ -311,7 +310,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline);
if (predicate.test(remoteSessionAdapter)) {
- InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+ InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
// Remote entity contains our predicate. Update local cache with the remote entity
SessionEntityWrapper<UserSessionEntity> sessionWrapper = remoteSessionEntity.mergeRemoteEntityWithLocalEntity(tx.get(id));
@@ -511,7 +510,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) {
LoginFailureKey key = new LoginFailureKey(realm.getId(), userId);
- return wrap(key, loginFailureCache.get(key));
+ LoginFailureEntity entity = getLoginFailureEntity(key);
+ return wrap(key, entity);
+ }
+
+ private LoginFailureEntity getLoginFailureEntity(LoginFailureKey key) {
+ InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> tx = getLoginFailuresTx();
+ SessionEntityWrapper<LoginFailureEntity> entityWrapper = tx.get(key);
+ return entityWrapper==null ? null : entityWrapper.getEntity();
}
@Override
@@ -520,13 +526,53 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
LoginFailureEntity entity = new LoginFailureEntity();
entity.setRealm(realm.getId());
entity.setUserId(userId);
- tx.put(loginFailureCache, key, entity);
+
+ SessionUpdateTask<LoginFailureEntity> createLoginFailureTask = new SessionUpdateTask<LoginFailureEntity>() {
+
+ @Override
+ public void runUpdate(LoginFailureEntity session) {
+
+ }
+
+ @Override
+ public CacheOperation getOperation(LoginFailureEntity session) {
+ return CacheOperation.ADD_IF_ABSENT;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<LoginFailureEntity> sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+
+ loginFailuresTx.addTask(key, createLoginFailureTask, entity);
+
return wrap(key, entity);
}
@Override
public void removeUserLoginFailure(RealmModel realm, String userId) {
- tx.remove(loginFailureCache, new LoginFailureKey(realm.getId(), userId));
+ SessionUpdateTask<LoginFailureEntity> removeTask = new SessionUpdateTask<LoginFailureEntity>() {
+
+ @Override
+ public void runUpdate(LoginFailureEntity entity) {
+
+ }
+
+ @Override
+ public CacheOperation getOperation(LoginFailureEntity entity) {
+ return CacheOperation.REMOVE;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<LoginFailureEntity> sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+
+ loginFailuresTx.addTask(new LoginFailureKey(realm.getId(), userId), removeTask);
}
@Override
@@ -543,9 +589,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private void removeAllLocalUserLoginFailuresEvent(String realmId) {
FuturesHelper futures = new FuturesHelper();
- Cache<LoginFailureKey, LoginFailureEntity> localCache = CacheDecorators.localCache(loginFailureCache);
+ Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> localCache = CacheDecorators.localCache(loginFailureCache);
- Cache<LoginFailureKey, LoginFailureEntity> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
+ Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
localCacheStoreIgnore
.entrySet()
@@ -593,8 +639,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
removeUserSessions(realm, user, true);
removeUserSessions(realm, user, false);
- loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getUsername()));
- loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail()));
+ removeUserLoginFailure(realm, user.getId());
}
@Override
@@ -602,7 +647,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) {
- InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+ InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
SessionUpdateTask<UserSessionEntity> removeTask = new SessionUpdateTask<UserSessionEntity>() {
@@ -626,17 +671,17 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
tx.addTask(sessionEntity.getId(), removeTask);
}
- InfinispanKeycloakTransaction getTx() {
- return tx;
+ InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> getLoginFailuresTx() {
+ return loginFailuresTx;
}
UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) {
- InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+ InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
return entity != null ? new UserSessionAdapter(session, this, tx, realm, entity, offline) : null;
}
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
- return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
+ return entity != null ? new UserLoginFailureAdapter(this, key, entity) : null;
}
UserSessionEntity getUserSessionEntity(UserSessionModel userSession, boolean offline) {
@@ -739,7 +784,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
- InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+ InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
SessionUpdateTask importTask = new SessionUpdateTask<UserSessionEntity>() {
@@ -775,7 +820,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession,
- InfinispanChangelogBasedTransaction<UserSessionEntity> updateTx) {
+ InfinispanChangelogBasedTransaction<String, UserSessionEntity> updateTx) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
entity.setAction(clientSession.getAction());
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index 489dd60..df30b4e 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -85,7 +85,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
- Cache<LoginFailureKey, LoginFailureEntity> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
+ Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, cache, offlineSessionsCache, loginFailures);
}
@@ -224,6 +224,11 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
if (offlineSessionsRemoteCache) {
offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
}
+
+ Cache loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
+ boolean loginFailuresRemoteCache = checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
+ return realm.getMaxDeltaTimeSeconds() * 1000;
+ });
}
private boolean checkRemoteCache(KeycloakSession session, Cache ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
index 89fd215..8891469 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
@@ -55,13 +55,13 @@ public class RemoteCacheInvoker {
}
- public <S extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, String key, SessionUpdateTask<S> task, SessionEntityWrapper<S> sessionWrapper) {
+ public <K, V extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) {
RemoteCacheContext context = remoteCaches.get(cacheName);
if (context == null) {
return;
}
- S session = sessionWrapper.getEntity();
+ V session = sessionWrapper.getEntity();
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
SessionUpdateTask.CrossDCMessageStatus status = task.getCrossDCMessageStatus(sessionWrapper);
@@ -82,8 +82,8 @@ public class RemoteCacheInvoker {
}
- private <S extends SessionEntity> void runOnRemoteCache(RemoteCache remoteCache, long maxIdleMs, String key, SessionUpdateTask<S> task, SessionEntityWrapper<S> sessionWrapper) {
- S session = sessionWrapper.getEntity();
+ private <K, V extends SessionEntity> void runOnRemoteCache(RemoteCache<K, V> remoteCache, long maxIdleMs, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) {
+ V session = sessionWrapper.getEntity();
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
switch (operation) {
@@ -96,13 +96,16 @@ public class RemoteCacheInvoker {
break;
case ADD_IF_ABSENT:
final int currentTime = Time.currentTime();
- SessionEntity existing = (SessionEntity) remoteCache
+ SessionEntity existing = remoteCache
.withFlags(Flag.FORCE_RETURN_VALUE)
.putIfAbsent(key, session, -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
if (existing != null) {
- throw new IllegalStateException("There is already existing value in cache for key " + key);
+ logger.debugf("Existing entity in remote cache for key: %s . Will update it", key);
+
+ replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task);
+ } else {
+ sessionWrapper.putLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE, currentTime);
}
- sessionWrapper.putLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE, currentTime);
break;
case REPLACE:
replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task);
@@ -113,16 +116,16 @@ public class RemoteCacheInvoker {
}
- private <S extends SessionEntity> void replace(RemoteCache remoteCache, long lifespanMs, long maxIdleMs, String key, SessionUpdateTask<S> task) {
+ private <K, V extends SessionEntity> void replace(RemoteCache<K, V> remoteCache, long lifespanMs, long maxIdleMs, K key, SessionUpdateTask<V> task) {
boolean replaced = false;
while (!replaced) {
- VersionedValue<S> versioned = remoteCache.getVersioned(key);
+ VersionedValue<V> versioned = remoteCache.getVersioned(key);
if (versioned == null) {
logger.warnf("Not found entity to replace for key '%s'", key);
return;
}
- S session = versioned.getValue();
+ V session = versioned.getValue();
// Run task on the remote session
task.runUpdate(session);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java
index d29e220..b186833 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java
@@ -44,12 +44,12 @@ import org.infinispan.client.hotrod.VersionedValue;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ClientListener
-public class RemoteCacheSessionListener {
+public class RemoteCacheSessionListener<K, V extends SessionEntity> {
protected static final Logger logger = Logger.getLogger(RemoteCacheSessionListener.class);
- private Cache<String, SessionEntityWrapper> cache;
- private RemoteCache remoteCache;
+ private Cache<K, SessionEntityWrapper<V>> cache;
+ private RemoteCache<K, V> remoteCache;
private boolean distributed;
private String myAddress;
@@ -58,7 +58,7 @@ public class RemoteCacheSessionListener {
}
- protected void init(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, RemoteCache remoteCache) {
+ protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, V> remoteCache) {
this.cache = cache;
this.remoteCache = remoteCache;
@@ -73,7 +73,7 @@ public class RemoteCacheSessionListener {
@ClientCacheEntryCreated
public void created(ClientCacheEntryCreatedEvent event) {
- String key = (String) event.getKey();
+ K key = (K) event.getKey();
if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
// Should load it from remoteStore
@@ -84,7 +84,7 @@ public class RemoteCacheSessionListener {
@ClientCacheEntryModified
public void updated(ClientCacheEntryModifiedEvent event) {
- String key = (String) event.getKey();
+ K key = (K) event.getKey();
if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
@@ -94,7 +94,7 @@ public class RemoteCacheSessionListener {
private static final int MAXIMUM_REPLACE_RETRIES = 10;
- private void replaceRemoteEntityInCache(String key, long eventVersion) {
+ private void replaceRemoteEntityInCache(K key, long eventVersion) {
// TODO can be optimized and remoteSession sent in the event itself?
boolean replaced = false;
int replaceRetries = 0;
@@ -102,8 +102,8 @@ public class RemoteCacheSessionListener {
do {
replaceRetries++;
- SessionEntityWrapper localEntityWrapper = cache.get(key);
- VersionedValue remoteSessionVersioned = remoteCache.getVersioned(key);
+ SessionEntityWrapper<V> localEntityWrapper = cache.get(key);
+ VersionedValue<V> remoteSessionVersioned = remoteCache.getVersioned(key);
if (remoteSessionVersioned == null || remoteSessionVersioned.getVersion() < eventVersion) {
try {
logger.debugf("Got replace remote entity event prematurely, will try again. Event version: %d, got: %d",
@@ -120,7 +120,7 @@ public class RemoteCacheSessionListener {
logger.debugf("Read session%s. Entity read from remote cache: %s", replaceRetries > 1 ? "" : " again", remoteSession);
- SessionEntityWrapper sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
+ SessionEntityWrapper<V> sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
// We received event from remoteCache, so we won't update it back
replaced = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
@@ -135,7 +135,7 @@ public class RemoteCacheSessionListener {
@ClientCacheEntryRemoved
public void removed(ClientCacheEntryRemovedEvent event) {
- String key = (String) event.getKey();
+ K key = (K) event.getKey();
if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
// We received event from remoteCache, so we won't update it back
@@ -152,7 +152,7 @@ public class RemoteCacheSessionListener {
// For distributed caches, ensure that local modification is executed just on owner OR if event.isCommandRetried
- protected boolean shouldUpdateLocalCache(ClientEvent.Type type, String key, boolean commandRetried) {
+ protected boolean shouldUpdateLocalCache(ClientEvent.Type type, K key, boolean commandRetried) {
boolean result;
// Case when cache is stopping or stopped already
@@ -184,7 +184,7 @@ public class RemoteCacheSessionListener {
}
- public static RemoteCacheSessionListener createListener(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, RemoteCache remoteCache) {
+ public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, V> remoteCache) {
/*boolean isCoordinator = InfinispanUtil.isCoordinator(cache);
// Just cluster coordinator will fetch userSessions from remote cache.
@@ -198,7 +198,7 @@ public class RemoteCacheSessionListener {
listener = new DontFetchInitialStateCacheListener();
}*/
- RemoteCacheSessionListener listener = new RemoteCacheSessionListener();
+ RemoteCacheSessionListener<K, V> listener = new RemoteCacheSessionListener<>();
listener.init(session, cache, remoteCache);
return listener;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
index 00c133a..789fc16 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
@@ -115,14 +115,14 @@ public class RemoteCacheSessionsLoader implements SessionLoader {
for (Map.Entry<byte[], byte[]> entry : remoteObjects.entrySet()) {
try {
- String key = (String) marshaller.objectFromByteBuffer(entry.getKey());
+ Object key = marshaller.objectFromByteBuffer(entry.getKey());
SessionEntity entity = (SessionEntity) marshaller.objectFromByteBuffer(entry.getValue());
SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity);
decoratedCache.putAsync(key, entityWrapper);
} catch (Exception e) {
- log.warnf("Error loading session from remote cache", e);
+ log.warn("Error loading session from remote cache", e);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
index f75391c..815a54d 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
@@ -48,7 +48,7 @@ public class Mappers {
return new UserSessionEntityMapper();
}
- public static Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey> loginFailureId() {
+ public static Function<Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>>, LoginFailureKey> loginFailureId() {
return new LoginFailureIdMapper();
}
@@ -103,9 +103,9 @@ public class Mappers {
}
- private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey>, Serializable {
+ private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>>, LoginFailureKey>, Serializable {
@Override
- public LoginFailureKey apply(Map.Entry<LoginFailureKey, LoginFailureEntity> entry) {
+ public LoginFailureKey apply(Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> entry) {
return entry.getKey();
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java
index ae0b28d..4996000 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java
@@ -17,6 +17,7 @@
package org.keycloak.models.sessions.infinispan.stream;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
@@ -27,7 +28,7 @@ import java.util.function.Predicate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class UserLoginFailurePredicate implements Predicate<Map.Entry<LoginFailureKey, LoginFailureEntity>>, Serializable {
+public class UserLoginFailurePredicate implements Predicate<Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>>>, Serializable {
private String realm;
@@ -40,8 +41,8 @@ public class UserLoginFailurePredicate implements Predicate<Map.Entry<LoginFailu
}
@Override
- public boolean test(Map.Entry<LoginFailureKey, LoginFailureEntity> entry) {
- LoginFailureEntity e = entry.getValue();
+ public boolean test(Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> entry) {
+ LoginFailureEntity e = entry.getValue().getEntity();
return realm.equals(e.getRealm());
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java
index a41777d..0971c26 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java
@@ -17,8 +17,8 @@
package org.keycloak.models.sessions.infinispan;
-import org.infinispan.Cache;
import org.keycloak.models.UserLoginFailureModel;
+import org.keycloak.models.sessions.infinispan.changes.LoginFailuresUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
@@ -28,13 +28,11 @@ import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
public class UserLoginFailureAdapter implements UserLoginFailureModel {
private InfinispanUserSessionProvider provider;
- private Cache<LoginFailureKey, LoginFailureEntity> cache;
private LoginFailureKey key;
private LoginFailureEntity entity;
- public UserLoginFailureAdapter(InfinispanUserSessionProvider provider, Cache<LoginFailureKey, LoginFailureEntity> cache, LoginFailureKey key, LoginFailureEntity entity) {
+ public UserLoginFailureAdapter(InfinispanUserSessionProvider provider, LoginFailureKey key, LoginFailureEntity entity) {
this.provider = provider;
- this.cache = cache;
this.key = key;
this.entity = entity;
}
@@ -51,8 +49,16 @@ public class UserLoginFailureAdapter implements UserLoginFailureModel {
@Override
public void setFailedLoginNotBefore(int notBefore) {
- entity.setFailedLoginNotBefore(notBefore);
- update();
+ LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+ @Override
+ public void runUpdate(LoginFailureEntity entity) {
+ entity.setFailedLoginNotBefore(notBefore);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -62,14 +68,30 @@ public class UserLoginFailureAdapter implements UserLoginFailureModel {
@Override
public void incrementFailures() {
- entity.setNumFailures(getNumFailures() + 1);
- update();
+ LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+ @Override
+ public void runUpdate(LoginFailureEntity entity) {
+ entity.setNumFailures(entity.getNumFailures() + 1);
+ }
+
+ };
+
+ update(task);
}
@Override
public void clearFailures() {
- entity.clearFailures();
- update();
+ LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+ @Override
+ public void runUpdate(LoginFailureEntity entity) {
+ entity.clearFailures();
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -79,8 +101,16 @@ public class UserLoginFailureAdapter implements UserLoginFailureModel {
@Override
public void setLastFailure(long lastFailure) {
- entity.setLastFailure(lastFailure);
- update();
+ LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+ @Override
+ public void runUpdate(LoginFailureEntity entity) {
+ entity.setLastFailure(lastFailure);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -90,12 +120,20 @@ public class UserLoginFailureAdapter implements UserLoginFailureModel {
@Override
public void setLastIPFailure(String ip) {
- entity.setLastIPFailure(ip);
- update();
+ LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+ @Override
+ public void runUpdate(LoginFailureEntity entity) {
+ entity.setLastIPFailure(ip);
+ }
+
+ };
+
+ update(task);
}
- void update() {
- provider.getTx().replace(cache, key, entity);
+ void update(LoginFailuresUpdateTask task) {
+ provider.getLoginFailuresTx().addTask(key, task);
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
index b00f2e4..7fd223f 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
@@ -146,7 +146,8 @@ public class RealmsAdminResource {
return Response.created(location).build();
} catch (ModelDuplicateException e) {
- return ErrorResponse.exists("Realm with same name exists");
+ logger.error("Conflict detected", e);
+ return ErrorResponse.exists("Conflict detected. See logs for details");
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index 0312cbb..846267f 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
@@ -462,26 +462,6 @@ public class UserSessionProviderTest {
}
- @Test
- public void testFailCreateExistingSession() {
- UserSessionModel userSession = session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
-
- // commit
- resetSession();
-
-
- try {
- session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
- kc.stopSession(session, true);
- Assert.fail("Not expected to successfully create duplicated userSession");
- } catch (IllegalStateException e) {
- // Expected
- session = kc.startSession();
- }
-
- }
-
-
private void testAuthenticatedClientSession(AuthenticatedClientSessionModel clientSession, String expectedClientId, String expectedUserSessionId, String expectedAction, int expectedTimestamp) {
Assert.assertEquals(expectedClientId, clientSession.getClient().getClientId());
Assert.assertEquals(expectedUserSessionId, clientSession.getUserSession().getId());
@@ -531,6 +511,15 @@ public class UserSessionProviderTest {
resetSession();
+ // Add the failure, which already exists
+ failure1 = session.sessions().addUserLoginFailure(realm, "user1");
+ failure1.incrementFailures();
+
+ resetSession();
+
+ failure1 = session.sessions().getUserLoginFailure(realm, "user1");
+ assertEquals(2, failure1.getNumFailures());
+
failure1 = session.sessions().getUserLoginFailure(realm, "user1");
failure1.clearFailures();
@@ -556,13 +545,15 @@ public class UserSessionProviderTest {
public void testOnUserRemoved() {
createSessions();
- session.sessions().addUserLoginFailure(realm, "user1");
- session.sessions().addUserLoginFailure(realm, "user1@localhost");
- session.sessions().addUserLoginFailure(realm, "user2");
+ UserModel user1 = session.users().getUserByUsername("user1", realm);
+ UserModel user2 = session.users().getUserByUsername("user2", realm);
+
+ session.sessions().addUserLoginFailure(realm, user1.getId());
+ session.sessions().addUserLoginFailure(realm, user2.getId());
resetSession();
- UserModel user1 = session.users().getUserByUsername("user1", realm);
+ user1 = session.users().getUserByUsername("user1", realm);
new UserManager(session).removeUser(realm, user1);
resetSession();
@@ -570,9 +561,8 @@ public class UserSessionProviderTest {
assertTrue(session.sessions().getUserSessions(realm, user1).isEmpty());
assertFalse(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty());
- assertNull(session.sessions().getUserLoginFailure(realm, "user1"));
- assertNull(session.sessions().getUserLoginFailure(realm, "user1@localhost"));
- assertNotNull(session.sessions().getUserLoginFailure(realm, "user2"));
+ assertNull(session.sessions().getUserLoginFailure(realm, user1.getId()));
+ assertNotNull(session.sessions().getUserLoginFailure(realm, user2.getId()));
}
private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java
new file mode 100644
index 0000000..ec572e9
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2017 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.crossdc;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import javax.ws.rs.NotFoundException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.Constants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserLoginFailureModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.client.KeycloakTestingClient;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class BruteForceCrossDCTest extends AbstractAdminCrossDCTest {
+
+ private static final String REALM_NAME = "brute-force-test";
+
+ @Before
+ public void beforeTest() {
+ try {
+ adminClient.realm(REALM_NAME).remove();
+ } catch (NotFoundException ignore) {
+ }
+
+ UserRepresentation user = UserBuilder.create()
+ .id("login-test-1")
+ .username("login-test-1")
+ .email("login-1@test.com")
+ .enabled(true)
+ .password("password")
+ .addRoles(Constants.OFFLINE_ACCESS_ROLE)
+ .build();
+
+ UserRepresentation user2 = UserBuilder.create()
+ .id("login-test-2")
+ .username("login-test-2")
+ .email("login-2@test.com")
+ .enabled(true)
+ .password("password")
+ .addRoles(Constants.OFFLINE_ACCESS_ROLE)
+ .build();
+
+ ClientRepresentation client = ClientBuilder.create()
+ .clientId("test-app")
+ .directAccessGrants()
+ .redirectUris("http://localhost:8180/auth/realms/master/app/*")
+ .addWebOrigin("http://localhost:8180")
+ .secret("password")
+ .build();
+
+ RealmRepresentation realmRep = RealmBuilder.create()
+ .name(REALM_NAME)
+ .user(user)
+ .user(user2)
+ .client(client)
+ .bruteForceProtected(true)
+ .build();
+
+ adminClient.realms().create(realmRep);
+ }
+
+
+ @Test
+ public void testBruteForceWithUserOperations() throws Exception {
+ // Enable 1st DC only
+ enableDcOnLoadBalancer(DC.FIRST);
+ enableDcOnLoadBalancer(DC.SECOND);
+
+ // Clear all
+ adminClient.realms().realm(REALM_NAME).attackDetection().clearAllBruteForce();
+ assertStatistics("After brute force cleared", 0, 0, 0);
+
+ // Create 10 brute force statuses for user1. Assert available on both DC1 and DC2
+ createBruteForceFailures(10, "login-test-1");
+ assertStatistics("After brute force for user1 created", 10, 0, 1);
+
+ // Create 10 brute force statuses for user2. Assert available on both DC1 and DC2createBruteForceFailures(10, "login-test-2");createBruteForceFailures(10, "login-test-2");
+ createBruteForceFailures(10, "login-test-2");
+ assertStatistics("After brute force for user2 created", 10, 10, 2);
+
+ // Remove brute force for user1
+ adminClient.realms().realm(REALM_NAME).attackDetection().clearBruteForceForUser("login-test-1");
+ assertStatistics("After brute force for user1 cleared", 0, 10, 1);
+
+ // Re-add 10 brute force statuses for user1
+ createBruteForceFailures(10, "login-test-1");
+ assertStatistics("After brute force for user1 re-created", 10, 10, 2);
+
+ // Remove user1
+ adminClient.realms().realm(REALM_NAME).users().get("login-test-1").remove();
+ assertStatistics("After user1 removed", 0, 10, 1);
+ }
+
+
+ @Test
+ public void testBruteForceWithRealmOperations() throws Exception {
+ // Enable 1st DC only
+ enableDcOnLoadBalancer(DC.FIRST);
+ enableDcOnLoadBalancer(DC.SECOND);
+
+ // Clear all
+ adminClient.realms().realm(REALM_NAME).attackDetection().clearAllBruteForce();
+ assertStatistics("After brute force cleared", 0, 0, 0);
+
+ // Create 10 brute force statuses for user1 and user2.
+ createBruteForceFailures(10, "login-test-1");
+ createBruteForceFailures(10, "login-test-2");
+ assertStatistics("After brute force for users created", 10, 10, 2);
+
+ // Clear all
+ adminClient.realms().realm(REALM_NAME).attackDetection().clearAllBruteForce();
+ assertStatistics("After brute force cleared for realm", 0, 0, 0);
+
+ // Re-add 10 brute force statuses for users
+ createBruteForceFailures(10, "login-test-1");
+ createBruteForceFailures(10, "login-test-2");
+ assertStatistics("After brute force for users re-created", 10, 10, 2);
+
+ // Remove realm
+ adminClient.realms().realm(REALM_NAME).remove();
+
+ Retry.execute(() -> {
+ int dc0CacheSize = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+ int dc1CacheSize = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+ Assert.assertEquals(0, dc0CacheSize);
+ Assert.assertEquals(0, dc1CacheSize);
+ }, 50, 50);
+
+ }
+
+
+ @Test
+ public void testDuplicatedPutIfAbsentOperation() throws Exception {
+ // Enable 1st DC only
+ enableDcOnLoadBalancer(DC.FIRST);
+ enableDcOnLoadBalancer(DC.SECOND);
+
+ // Clear all
+ adminClient.realms().realm(REALM_NAME).attackDetection().clearAllBruteForce();
+ assertStatistics("After brute force cleared", 0, 0, 0);
+
+ // create the entry manually in DC0
+ addUserLoginFailure(getTestingClientForStartedNodeInDc(0));
+ assertStatistics("After create entry1", 1, 0, 1);
+
+ // try to create the entry manually in DC1 (not use real concurrency for now). It should still update the numFailures in existing entry rather then override it
+ addUserLoginFailure(getTestingClientForStartedNodeInDc(1));
+ assertStatistics("After create entry2", 2, 0, 1);
+
+ }
+
+
+ private void assertStatistics(String prefixMessage, int expectedUser1, int expectedUser2, int expectedCacheSize) {
+ Retry.execute(() -> {
+ int dc0user1 = (Integer) getAdminClientForStartedNodeInDc(0).realm(REALM_NAME).attackDetection().bruteForceUserStatus("login-test-1").get("numFailures");
+ int dc1user1 = (Integer) getAdminClientForStartedNodeInDc(1).realm(REALM_NAME).attackDetection().bruteForceUserStatus("login-test-1").get("numFailures");
+ int dc0user2 = (Integer) getAdminClientForStartedNodeInDc(0).realm(REALM_NAME).attackDetection().bruteForceUserStatus("login-test-2").get("numFailures");
+ int dc1user2 = (Integer) getAdminClientForStartedNodeInDc(1).realm(REALM_NAME).attackDetection().bruteForceUserStatus("login-test-2").get("numFailures");
+
+ int dc0CacheSize = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+ int dc1CacheSize = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+
+ log.infof("%s: dc0User1=%d, dc0user2=%d, dc1user1=%d, dc1user2=%d, dc0CacheSize=%d, dc1CacheSize=%d", prefixMessage, dc0user1, dc0user2, dc1user1, dc1user2, dc0CacheSize, dc1CacheSize);
+
+ Assert.assertEquals(dc0user1, expectedUser1);
+ Assert.assertEquals(dc0user2, expectedUser2);
+ Assert.assertEquals(dc1user1, expectedUser1);
+ Assert.assertEquals(dc1user2, expectedUser2);
+
+ Assert.assertEquals(expectedCacheSize, dc0CacheSize);
+ Assert.assertEquals(expectedCacheSize, dc1CacheSize);
+ }, 50, 50);
+ }
+
+
+
+
+
+ private void createBruteForceFailures(int count, String username) throws Exception {
+ oauth.realm(REALM_NAME);
+
+ for (int i=0 ; i<count ; i++) {
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", username, "bad-password");
+ Assert.assertNull(response.getAccessToken());
+ Assert.assertNotNull(response.getError());
+ }
+
+ }
+
+
+ // TODO Having this working on Wildfly might be a challenge. Maybe require @Deployment with @TargetsContainer descriptor generated at runtime as we don't know the container qualifier at compile time... Maybe workaround by add endpoint to TestingResourceProvider if needed..
+ private void addUserLoginFailure(KeycloakTestingClient testingClient) throws URISyntaxException, IOException {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName(REALM_NAME);
+ UserLoginFailureModel loginFailure = session.sessions().addUserLoginFailure(realm, "login-test-1");
+ loginFailure.incrementFailures();
+ });
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
index a73d283..80949cf 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
@@ -23,7 +23,9 @@ import java.util.List;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.crossdc.AbstractAdminCrossDCTest;
import org.keycloak.testsuite.crossdc.DC;
@@ -187,6 +189,52 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest {
}
+ @Test
+ public void loginFailuresPreloadTest() throws Exception {
+ // Enable brute force protector
+ RealmRepresentation realmRep = getAdminClientForStartedNodeInDc(0).realms().realm("test").toRepresentation();
+ realmRep.setBruteForceProtected(true);
+ getAdminClientForStartedNodeInDc(0).realms().realm("test").update(realmRep);
+
+ String userId = ApiUtil.findUserByUsername(getAdminClientForStartedNodeInDc(0).realms().realm("test"), "test-user@localhost").getId();
+
+ int loginFailuresBefore = (Integer) getAdminClientForStartedNodeInDc(0).realm("test").attackDetection().bruteForceUserStatus(userId).get("numFailures");
+ log.infof("loginFailuresBefore: %d", loginFailuresBefore);
+
+ // Create initial brute force records
+ for (int i=0 ; i<SESSIONS_COUNT ; i++) {
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "bad-password");
+ Assert.assertNull(response.getAccessToken());
+ Assert.assertNotNull(response.getError());
+ }
+
+ // Start 2nd DC.
+ containerController.start(getCacheServer(DC.SECOND).getQualifier());
+ startBackendNode(DC.SECOND, 0);
+ enableLoadBalancerNode(DC.SECOND, 0);
+
+ // Ensure loginFailures are loaded in both 1st DC and 2nd DC
+ int size1 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+ int size2 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+ int loginFailures1 = (Integer) getAdminClientForStartedNodeInDc(0).realm("test").attackDetection().bruteForceUserStatus(userId).get("numFailures");
+ int loginFailures2 = (Integer) getAdminClientForStartedNodeInDc(1).realm("test").attackDetection().bruteForceUserStatus(userId).get("numFailures");
+ log.infof("size1: %d, size2: %d, loginFailures1: %d, loginFailures2: %d", size1, size2, loginFailures1, loginFailures2);
+ Assert.assertEquals(size1, 1);
+ Assert.assertEquals(size2, 1);
+ Assert.assertEquals(loginFailures1, loginFailuresBefore + SESSIONS_COUNT);
+ Assert.assertEquals(loginFailures2, loginFailuresBefore + SESSIONS_COUNT);
+
+ // On DC2 sessions were preloaded from from remoteCache
+ Assert.assertTrue(getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.WORK_CACHE_NAME).contains("distributed::remoteCacheLoad::loginFailures"));
+
+ // Disable brute force protector
+ realmRep = getAdminClientForStartedNodeInDc(0).realms().realm("test").toRepresentation();
+ realmRep.setBruteForceProtected(true);
+ getAdminClientForStartedNodeInDc(0).realms().realm("test").update(realmRep);
+ }
+
+
+
private List<OAuthClient.AccessTokenResponse> createInitialSessions(boolean offline) throws Exception {
if (offline) {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);