diff --git a/dataset/src/main/java/org/keycloak/benchmark/cache/CacheResource.java b/dataset/src/main/java/org/keycloak/benchmark/cache/CacheResource.java index 5eb74ef5a..e1b24440c 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/cache/CacheResource.java +++ b/dataset/src/main/java/org/keycloak/benchmark/cache/CacheResource.java @@ -20,12 +20,14 @@ import java.util.UUID; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import org.infinispan.Cache; import org.infinispan.commons.CacheConfigurationException; import org.jboss.logging.Logger; @@ -85,6 +87,31 @@ public boolean contains(@PathParam("id") String id) { } } + @GET + @Path("/remove/{id}") + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public boolean remove(@PathParam("id") String id, @QueryParam("skipListeners") @DefaultValue("false") boolean skipListeners) { + if (decorateCacheForRemovalAndSkipListenersIfTrue(skipListeners).remove(id) != null) { + return true; + } else if (id.length() == 36) { + try { + UUID uuid = UUID.fromString(id); + return decorateCacheForRemovalAndSkipListenersIfTrue(skipListeners).remove(uuid) != null; + } catch (IllegalArgumentException iae) { + logger.warnf("Given string %s not an UUID", id); + return false; + } + } else { + return false; + } + } + + public Cache decorateCacheForRemovalAndSkipListenersIfTrue(boolean skipListeners) { + return skipListeners + ? cache.getAdvancedCache().withFlags(org.infinispan.context.Flag.SKIP_CACHE_STORE) + : cache.getAdvancedCache(); + } @GET @Path("/size") @NoCache diff --git a/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/AbstractCrossDCTest.java b/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/AbstractCrossDCTest.java index dce1dcda1..e40745cdb 100644 --- a/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/AbstractCrossDCTest.java +++ b/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/AbstractCrossDCTest.java @@ -37,7 +37,7 @@ public abstract class AbstractCrossDCTest { protected static HttpClient HTTP_CLIENT = HttpClientUtils.newHttpClient(); protected static final DatacenterInfo DC_1, DC_2; protected static final KeycloakClient LOAD_BALANCER_KEYCLOAK; - public static String ISPN_USERNAME = System.getProperty("infinispan.username", "developer");; + public static String ISPN_USERNAME = System.getProperty("infinispan.username", "developer"); public static final String REALM_NAME = "cross-dc-test-realm"; public static final String CLIENTID = "cross-dc-test-client"; public static final String CLIENT_SECRET = "cross-dc-test-client-secret"; @@ -109,10 +109,23 @@ public void setUpTestEnvironment() throws UnknownHostException { realmResource.users().create(user).close(); + clearCache(DC_1, SESSIONS); + clearCache(DC_1, CLIENT_SESSIONS); + clearCache(DC_2, SESSIONS); + clearCache(DC_2, CLIENT_SESSIONS); assertCacheSize(SESSIONS, 0); assertCacheSize(CLIENT_SESSIONS, 0); } + private void clearCache(DatacenterInfo dc, String cache) { + dc.kc().embeddedIspn().cache(cache).clear(); + dc.ispn().cache(cache).clear(); + if (cache.equals(SESSIONS) || cache.equals(CLIENT_SESSIONS)) { + // those sessions will have been invalidated + KeycloakClient.cleanAdminClients(); + } + } + @AfterEach public void tearDownTestEnvironment() throws URISyntaxException, IOException, InterruptedException { Keycloak adminClient = DC_1.kc().adminClient(); diff --git a/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/LoginLogoutTest.java b/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/LoginLogoutTest.java index a523d45e9..901cab1bb 100644 --- a/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/LoginLogoutTest.java +++ b/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/LoginLogoutTest.java @@ -1,5 +1,6 @@ package org.keycloak.benchmark.crossdc; +import org.jboss.logging.Logger; import org.junit.jupiter.api.Test; import org.keycloak.benchmark.crossdc.util.InfinispanUtils; @@ -16,6 +17,9 @@ public class LoginLogoutTest extends AbstractCrossDCTest { + + protected static final Logger LOG = Logger.getLogger(LoginLogoutTest.class); + @Test public void loginLogoutTest() throws URISyntaxException, IOException, InterruptedException { //Login and exchange code in DC1 @@ -127,7 +131,7 @@ public void testRemoteStoreDiscrepancyMissingSessionInBackupRemoteISPN() throws assertTrue(DC_2.kc().embeddedIspn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); assertTrue(DC_2.ispn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); - // Remove the session from the remote store in DC1 only + // Remove the session from the remote store in DC2 only try (var close = InfinispanUtils.withBackupDisabled(DC_2.ispn().cache(SESSIONS), DC_1.ispn().siteName())) { assertFalse(DC_2.ispn().cache(SESSIONS).isBackupOnline(DC_1.ispn().siteName())); DC_2.ispn().cache(SESSIONS).remove((String) tokensMap.get("session_state")); @@ -148,4 +152,47 @@ public void testRemoteStoreDiscrepancyMissingSessionInBackupRemoteISPN() throws assertFalse(DC_2.kc().embeddedIspn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); assertFalse(DC_2.ispn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); } + + @Test + public void testRemoteStoreDiscrepancyMissingSessionInAllRemoteISPN() throws URISyntaxException, IOException, InterruptedException { + // Create a new user session + Map tokensMap = LOAD_BALANCER_KEYCLOAK.passwordGrant(REALM_NAME, CLIENTID, USERNAME, MAIN_PASSWORD); + LOG.info("processing session " + tokensMap.get("session_state")); + + // Make sure all ISPNs can see the entry in the cache + assertTrue(DC_1.kc().embeddedIspn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertTrue(DC_1.ispn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertTrue(DC_2.kc().embeddedIspn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertTrue(DC_2.ispn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + + // Remove the session from the remote store in DC1 only + try (var close = InfinispanUtils.withBackupDisabled(DC_1.ispn().cache(SESSIONS), DC_2.ispn().siteName())) { + assertFalse(DC_1.ispn().cache(SESSIONS).isBackupOnline(DC_2.ispn().siteName())); + DC_1.ispn().cache(SESSIONS).remove((String) tokensMap.get("session_state")); + } catch (Exception e) { + throw new RuntimeException(e); + } + assertTrue(DC_1.ispn().cache(SESSIONS).isBackupOnline(DC_2.ispn().siteName())); + + // Remove the session from the remote store in DC2 only + try (var close = InfinispanUtils.withBackupDisabled(DC_2.ispn().cache(SESSIONS), DC_1.ispn().siteName())) { + assertFalse(DC_2.ispn().cache(SESSIONS).isBackupOnline(DC_1.ispn().siteName())); + DC_2.ispn().cache(SESSIONS).remove((String) tokensMap.get("session_state")); + } catch (Exception e) { + throw new RuntimeException(e); + } + assertTrue(DC_2.ispn().cache(SESSIONS).isBackupOnline(DC_1.ispn().siteName())); + + assertTrue(DC_1.kc().embeddedIspn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertFalse(DC_1.ispn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertTrue(DC_2.kc().embeddedIspn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertFalse(DC_2.ispn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + + LOAD_BALANCER_KEYCLOAK.logout(REALM_NAME, (String) tokensMap.get("id_token"), CLIENTID); + + assertFalse(DC_1.kc().embeddedIspn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertFalse(DC_1.ispn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertFalse(DC_2.kc().embeddedIspn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + assertFalse(DC_2.ispn().cache(SESSIONS).contains((String) tokensMap.get("session_state"))); + } } diff --git a/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/client/ExternalInfinispanClient.java b/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/client/ExternalInfinispanClient.java index f72441665..b5581618f 100644 --- a/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/client/ExternalInfinispanClient.java +++ b/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/client/ExternalInfinispanClient.java @@ -150,12 +150,13 @@ public boolean remove(String key) { .build(); - HttpResponse response = null; + HttpResponse response; try { response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } + assertEquals(200, response.statusCode()); return Boolean.parseBoolean(response.body()); } diff --git a/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/client/KeycloakClient.java b/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/client/KeycloakClient.java index cc979eada..49ae4c539 100644 --- a/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/client/KeycloakClient.java +++ b/provision/rosa-cross-dc/keycloak-benchmark-crossdc-tests/src/test/java/org/keycloak/benchmark/crossdc/client/KeycloakClient.java @@ -265,7 +265,18 @@ public long size() { @Override public void clear() { - throw new NotImplementedYetException("This is not yet implemented :/"); + try { + URI uri = new URIBuilder(testRealmUrl("master") + "/cache/" + name + "/clear").build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(uri) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } } @Override @@ -282,7 +293,27 @@ public boolean contains(String key) throws URISyntaxException, IOException, Inte @Override public boolean remove(String key) { - throw new NotImplementedYetException("This is not yet implemented :/"); + URI uri = null; + try { + uri = new URIBuilder( testRealmUrl("master") + "/cache/" + name + "/remove/" + key + "?skipListeners=true").build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(uri) + .GET() + .build(); + + + HttpResponse response; + try { + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + assertEquals(200, response.statusCode()); + return Boolean.parseBoolean(response.body()); } @Override