From b6d5cf4cc8512de085f0f595bac5e6592845acac Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Thu, 23 Oct 2025 18:03:41 +0200 Subject: [PATCH 1/5] Avoid touching the database layer if no changes are necessary for a user Closes #43682 Signed-off-by: Alexander Schwartz --- .../models/cache/infinispan/UserAdapter.java | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index a3a72aca5496..d7dbf044a03f 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -170,6 +170,9 @@ public boolean isEnabled() { @Override public void setEnabled(boolean enabled) { + if (updated == null && cached.isEnabled() == enabled) { + return; + } getDelegateForUpdate(); updated.setEnabled(enabled); } @@ -214,10 +217,11 @@ public void setAttribute(String name, List values) { @Override public void removeAttribute(String name) { - if (getFirstAttribute(name) != null) { - getDelegateForUpdate(); - updated.removeAttribute(name); + if (updated == null && getFirstAttribute(name) == null) { + return; } + getDelegateForUpdate(); + updated.removeAttribute(name); } @Override @@ -247,30 +251,38 @@ public Stream getRequiredActionsStream() { @Override public void addRequiredAction(RequiredAction action) { + if (updated == null && getRequiredActionsStream().anyMatch(s -> Objects.equals(s, action.name()))) { + return; + } getDelegateForUpdate(); updated.addRequiredAction(action); } @Override public void removeRequiredAction(RequiredAction action) { - if (getRequiredActionsStream().anyMatch(s -> Objects.equals(s, action.name()))) { - getDelegateForUpdate(); - updated.removeRequiredAction(action); + if (updated == null && getRequiredActionsStream().noneMatch(s -> Objects.equals(s, action.name()))) { + return; } + getDelegateForUpdate(); + updated.removeRequiredAction(action); } @Override public void addRequiredAction(String action) { + if (updated == null && getRequiredActionsStream().anyMatch(s -> Objects.equals(s, action))) { + return; + } getDelegateForUpdate(); updated.addRequiredAction(action); } @Override public void removeRequiredAction(String action) { - if (getRequiredActionsStream().anyMatch(s -> Objects.equals(s, action))) { - getDelegateForUpdate(); - updated.removeRequiredAction(action); + if (updated == null && getRequiredActionsStream().noneMatch(s -> Objects.equals(s, action))) { + return; } + getDelegateForUpdate(); + updated.removeRequiredAction(action); } @Override @@ -281,6 +293,9 @@ public boolean isEmailVerified() { @Override public void setEmailVerified(boolean verified) { + if (updated == null && cached.isEmailVerified() == verified) { + return; + } getDelegateForUpdate(); updated.setEmailVerified(verified); } @@ -293,6 +308,9 @@ public String getFederationLink() { @Override public void setFederationLink(String link) { + if (updated == null && Objects.equals(cached.getFederationLink(), link)) { + return; + } getDelegateForUpdate(); updated.setFederationLink(link); } @@ -305,6 +323,9 @@ public String getServiceAccountClientLink() { @Override public void setServiceAccountClientLink(String clientInternalId) { + if (updated == null && Objects.equals(cached.getServiceAccountClientLink(), clientInternalId)) { + return; + } getDelegateForUpdate(); updated.setServiceAccountClientLink(clientInternalId); } @@ -388,6 +409,9 @@ public boolean hasRole(RoleModel role) { @Override public void grantRole(RoleModel role) { + if (updated == null && cached.getRoleMappings(keycloakSession, modelSupplier).contains(role.getId())) { + return; + } getDelegateForUpdate(); updated.grantRole(role); } @@ -411,6 +435,9 @@ public Stream getRoleMappingsStream() { @Override public void deleteRoleMapping(RoleModel role) { + if (updated == null && !cached.getRoleMappings(keycloakSession, modelSupplier).contains(role.getId())) { + return; + } getDelegateForUpdate(); updated.deleteRoleMapping(role); } @@ -454,6 +481,9 @@ public long getGroupsCountByNameContaining(String search) { @Override public void joinGroup(GroupModel group) { + if (group.getType() == Type.REALM && cached.getGroups(keycloakSession, modelSupplier).contains(group.getId())) { + return; + } getDelegateForUpdate(); updated.joinGroup(group); @@ -461,6 +491,9 @@ public void joinGroup(GroupModel group) { @Override public void leaveGroup(GroupModel group) { + if (group.getType() == Type.REALM && updated == null && !cached.getGroups(keycloakSession, modelSupplier).contains(group.getId())) { + return; + } getDelegateForUpdate(); updated.leaveGroup(group); } From 261b7f55a155c7eb323ad95b4dc5fb4e0ea8f138 Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Fri, 24 Oct 2025 10:18:02 +0200 Subject: [PATCH 2/5] Fixing test Signed-off-by: Alexander Schwartz --- .../keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java | 1 + .../testsuite/federation/ldap/LDAPProvidersIntegrationTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java index 4d7c8a424ca6..1ef36debd265 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiTest.java @@ -259,6 +259,7 @@ public void testErrorResponseWhenLdapIsFailing() { UserResource userResource = testRealm().users().get(newUserId1); try { + user1.setFirstName(user1.getFirstName() + " updated"); userResource.update(user1); Assert.fail("Not expected to successfully update user"); } catch (WebApplicationException expected) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java index 27373071fd40..ed627c32f25a 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java @@ -1658,6 +1658,7 @@ public void updateLDAPUsernameTest() { RealmModel testRealm = ctx.getRealm(); UserModel importedUser = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(testRealm, "beckybecks"); + Assert.assertNotNull(importedUser); // Update user 'beckybecks' in LDAP LDAPObject becky = ctx.getLdapProvider().loadLDAPUserByUsername(testRealm, importedUser.getUsername()); From f7548dd28fb865dcb8a6d3f1c99b56d857ab8eb8 Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Fri, 24 Oct 2025 13:37:16 +0200 Subject: [PATCH 3/5] Review Signed-off-by: Alexander Schwartz --- .../models/cache/infinispan/UserAdapter.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index d7dbf044a03f..aadead506eab 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -251,7 +251,7 @@ public Stream getRequiredActionsStream() { @Override public void addRequiredAction(RequiredAction action) { - if (updated == null && getRequiredActionsStream().anyMatch(s -> Objects.equals(s, action.name()))) { + if (action == null || updated == null && getCachedRequiredActions().contains(action.name())) { return; } getDelegateForUpdate(); @@ -260,7 +260,7 @@ public void addRequiredAction(RequiredAction action) { @Override public void removeRequiredAction(RequiredAction action) { - if (updated == null && getRequiredActionsStream().noneMatch(s -> Objects.equals(s, action.name()))) { + if (action == null || updated == null && !getCachedRequiredActions().contains(action.name())) { return; } getDelegateForUpdate(); @@ -269,7 +269,7 @@ public void removeRequiredAction(RequiredAction action) { @Override public void addRequiredAction(String action) { - if (updated == null && getRequiredActionsStream().anyMatch(s -> Objects.equals(s, action))) { + if (updated == null && getCachedRequiredActions().contains(action)) { return; } getDelegateForUpdate(); @@ -278,13 +278,17 @@ public void addRequiredAction(String action) { @Override public void removeRequiredAction(String action) { - if (updated == null && getRequiredActionsStream().noneMatch(s -> Objects.equals(s, action))) { + if (updated == null && !getCachedRequiredActions().contains(action)) { return; } getDelegateForUpdate(); updated.removeRequiredAction(action); } + private Set getCachedRequiredActions() { + return cached.getRequiredActions(keycloakSession, modelSupplier); + } + @Override public boolean isEmailVerified() { if (updated != null) return updated.isEmailVerified(); From d1e07d3e47befdc54c2510c723171a5d3b18305c Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Fri, 24 Oct 2025 21:37:59 +0200 Subject: [PATCH 4/5] Fixing readonly Signed-off-by: Alexander Schwartz --- .../models/cache/infinispan/UserCacheSession.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 8480d141c2c1..0d9de2cd6fb2 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 @@ -376,7 +376,13 @@ protected UserModel validateCache(RealmModel realm, CachedUser cached, Supplier< } } - return new UserAdapter(cached, this, session, realm); + UserAdapter userAdapter = new UserAdapter(cached, this, session, realm); + + if (isReadOnlyOrganizationMember(session, userAdapter)) { + return new ReadOnlyUserModelDelegate(userAdapter, false); + } + + return userAdapter; } protected UserModel cacheUser(RealmModel realm, UserModel delegate, Long revision) { From a5b508f6a0bdbb4d3a61730252f2ce7f6b24a36d Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Sun, 26 Oct 2025 20:35:08 +0100 Subject: [PATCH 5/5] Fixing test Signed-off-by: Alexander Schwartz --- .../sessions/infinispan/changes/JpaChangesPerformer.java | 2 ++ .../org/keycloak/testsuite/model/UserSessionProviderTest.java | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java index f7dfdd17c928..a615cb83de59 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java @@ -169,6 +169,7 @@ private static void processClientSessionUpdate(Keyc SessionUpdatesList sessionUpdates = entry.getValue(); SessionEntityWrapper sessionWrapper = sessionUpdates.getEntityWrapper(); RealmModel realm = sessionUpdates.getRealm(); + session.getContext().setRealm(realm); UserSessionPersisterProvider userSessionPersister = session.getProvider(UserSessionPersisterProvider.class); switch (merged.getOperation()) { @@ -433,6 +434,7 @@ private static void processUserSessionUpdate(Keyclo SessionUpdatesList sessionUpdates = entry.getValue(); SessionEntityWrapper sessionWrapper = sessionUpdates.getEntityWrapper(); RealmModel realm = sessionUpdates.getRealm(); + session.getContext().setRealm(realm); UserSessionPersisterProvider userSessionPersister = session.getProvider(UserSessionPersisterProvider.class); UserSessionEntity entity = (UserSessionEntity) sessionWrapper.getEntity(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index d65800e0caed..ced3ad150b52 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -146,7 +146,7 @@ public void testRestartSession(KeycloakSession session) { Time.setOffset(100); try { KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), kcSession -> { - kcSession.getContext().setRealm(realm); + kcSession.getContext().setRealm(session.realms().getRealm(realm.getId())); UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessions[1].getId()); assertSession(userSession, kcSession.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(realm.getClientByClientId("test-app").getId()); @@ -166,6 +166,7 @@ public void testRestartSession(KeycloakSession session) { }); KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), kcSession -> { + kcSession.getContext().setRealm(session.realms().getRealm(realm.getId())); UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessions[1].getId()); assertThat(userSession.getNotes(), Matchers.anEmptyMap());