From 73e2e895577dfe96b5549212a1a2b56ebd377c3f Mon Sep 17 00:00:00 2001 From: Prahlad Bhakat Date: Tue, 11 Aug 2026 17:04:21 +0530 Subject: [PATCH 1/2] Filter stale role IDs in Infinispan client adapters Signed-off-by: Prahlad Bhakat --- .../org/keycloak/models/cache/infinispan/ClientAdapter.java | 3 ++- .../keycloak/models/cache/infinispan/ClientScopeAdapter.java | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 5a9e2e3e84fa..2340b3d56eef 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 @@ -254,7 +254,8 @@ public void setFullScopeAllowed(boolean value) { public Stream getScopeMappingsStream() { if (isUpdated()) return updated.getScopeMappingsStream(); return cached.getScope().stream() - .map(id -> cacheSession.getRoleById(cachedRealm, id)); + .map(id -> cacheSession.getRoleById(cachedRealm, id)) + .filter(Objects::nonNull); } public void addScopeMapping(RoleModel role) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientScopeAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientScopeAdapter.java index 2ec99ab65aed..7435303f8c02 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientScopeAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientScopeAdapter.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; import java.util.stream.Stream; @@ -164,7 +165,8 @@ public void setProtocol(String protocol) { public Stream getScopeMappingsStream() { if (isUpdated()) return updated.getScopeMappingsStream(); return cached.getScope().stream() - .map(id -> cacheSession.getRoleById(cachedRealm, id)); + .map(id -> cacheSession.getRoleById(cachedRealm, id)) + .filter(Objects::nonNull); } @Override From 0b6e844fbc59f3f62d2ca8c705e883efc0df239f Mon Sep 17 00:00:00 2001 From: Prahlad Bhakat Date: Wed, 12 Aug 2026 09:29:51 +0530 Subject: [PATCH 2/2] Add regression tests for stale cached role ID filtering Signed-off-by: Prahlad Bhakat --- model/infinispan/pom.xml | 22 +++++++++++ .../cache/infinispan/ClientAdapterTest.java | 38 +++++++++++++++++++ .../infinispan/ClientScopeAdapterTest.java | 38 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 model/infinispan/src/test/java/org/keycloak/models/cache/infinispan/ClientAdapterTest.java create mode 100644 model/infinispan/src/test/java/org/keycloak/models/cache/infinispan/ClientScopeAdapterTest.java diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml index 3f57fbac45ea..e05b10d4519a 100755 --- a/model/infinispan/pom.xml +++ b/model/infinispan/pom.xml @@ -96,6 +96,28 @@ test + + junit + junit + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + org.eclipse.microprofile.metrics diff --git a/model/infinispan/src/test/java/org/keycloak/models/cache/infinispan/ClientAdapterTest.java b/model/infinispan/src/test/java/org/keycloak/models/cache/infinispan/ClientAdapterTest.java new file mode 100644 index 000000000000..67c90cd6d54e --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/models/cache/infinispan/ClientAdapterTest.java @@ -0,0 +1,38 @@ +package org.keycloak.models.cache.infinispan; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.cache.infinispan.entities.CachedClient; + +class ClientAdapterTest { + + @Test + void getScopeMappingsStreamFiltersUnresolvableRoleIds() { + RealmModel realm = mock(RealmModel.class); + RealmCacheSession cacheSession = mock(RealmCacheSession.class); + CachedClient cached = mock(CachedClient.class); + + RoleModel liveRole = mock(RoleModel.class); + when(cached.getScope()).thenReturn(Set.of("stale-role-id", "live-role-id")); + // simulates a role deleted on another node whose cache invalidation + // hasn't reached this one yet + when(cacheSession.getRoleById(eq(realm), eq("stale-role-id"))).thenReturn(null); + when(cacheSession.getRoleById(eq(realm), eq("live-role-id"))).thenReturn(liveRole); + + ClientAdapter adapter = new ClientAdapter(realm, cached, cacheSession); + + List result = adapter.getScopeMappingsStream().toList(); + + assertEquals(List.of(liveRole), result, + "a scope mapping pointing at an unresolvable role must be silently dropped, not surfaced as null"); + } +} diff --git a/model/infinispan/src/test/java/org/keycloak/models/cache/infinispan/ClientScopeAdapterTest.java b/model/infinispan/src/test/java/org/keycloak/models/cache/infinispan/ClientScopeAdapterTest.java new file mode 100644 index 000000000000..b21cd68da004 --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/models/cache/infinispan/ClientScopeAdapterTest.java @@ -0,0 +1,38 @@ +package org.keycloak.models.cache.infinispan; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.cache.infinispan.entities.CachedClientScope; + +class ClientScopeAdapterTest { + + @Test + void getScopeMappingsStreamFiltersUnresolvableRoleIds() { + RealmModel realm = mock(RealmModel.class); + RealmCacheSession cacheSession = mock(RealmCacheSession.class); + CachedClientScope cached = mock(CachedClientScope.class); + + RoleModel liveRole = mock(RoleModel.class); + when(cached.getScope()).thenReturn(Set.of("stale-role-id", "live-role-id")); + // simulates a role deleted on another node whose cache invalidation + // hasn't reached this one yet + when(cacheSession.getRoleById(eq(realm), eq("stale-role-id"))).thenReturn(null); + when(cacheSession.getRoleById(eq(realm), eq("live-role-id"))).thenReturn(liveRole); + + ClientScopeAdapter adapter = new ClientScopeAdapter(realm, cached, cacheSession); + + List result = adapter.getScopeMappingsStream().toList(); + + assertEquals(List.of(liveRole), result, + "a scope mapping pointing at an unresolvable role must be silently dropped, not surfaced as null"); + } +}