From 84f4bd8af19b7f607b41ff4f878718c9e6c08ba8 Mon Sep 17 00:00:00 2001 From: Pedro Ruivo Date: Wed, 23 Oct 2024 15:59:37 +0100 Subject: [PATCH] Client Scope updates are not replicated between Keycloak nodes Fixes #33731 Signed-off-by: Pedro Ruivo --- .../cache/infinispan/RealmCacheSession.java | 1 + .../ClientScopeInvalidationClusterTest.java | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ClientScopeInvalidationClusterTest.java diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index af9fea6f8e..7ecc221de8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -264,6 +264,7 @@ public class RealmCacheSession implements CacheRealmProvider { public void registerClientScopeInvalidation(String id, String realmId) { invalidateClientScope(id); cache.clientScopeUpdated(realmId, invalidations); + invalidationEvents.add(new CacheKeyInvalidatedEvent(id)); } private void invalidateClientScope(String id) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ClientScopeInvalidationClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ClientScopeInvalidationClusterTest.java new file mode 100644 index 0000000000..b9f829e16d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ClientScopeInvalidationClusterTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 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.cluster; + +import java.util.Map; + +import jakarta.ws.rs.NotFoundException; +import org.keycloak.admin.client.resource.ClientScopeResource; +import org.keycloak.admin.client.resource.ClientScopesResource; +import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.ContainerInfo; + +import static org.junit.Assert.assertNull; + +public class ClientScopeInvalidationClusterTest extends AbstractInvalidationClusterTestWithTestRealm { + @Override + protected ClientScopeRepresentation createTestEntityRepresentation() { + var clientScope = new ClientScopeRepresentation(); + clientScope.setName("client-scope-name"); + clientScope.setDescription("client-scope-description"); + clientScope.setProtocol("openid-connect"); + clientScope.setAttributes(Map.of("a", "b", "c", "d")); + return clientScope; + } + + @Override + protected ClientScopeResource entityResource(ClientScopeRepresentation testEntity, ContainerInfo node) { + return entityResource(testEntity.getId(), node); + } + + @Override + protected ClientScopeResource entityResource(String idOrName, ContainerInfo node) { + return clientScopes(node).get(idOrName); + } + + @Override + protected ClientScopeRepresentation createEntity(ClientScopeRepresentation testEntity, ContainerInfo node) { + try (var rsp = clientScopes(node).create(testEntity)) { + testEntity.setId(ApiUtil.getCreatedId(rsp)); + } + return readEntity(testEntity, node); + } + + @Override + protected ClientScopeRepresentation readEntity(ClientScopeRepresentation entity, ContainerInfo node) { + try { + return entityResource(entity, node).toRepresentation(); + } catch (NotFoundException nfe) { + // expected when client scope doesn't exist + return null; + } + } + + @Override + protected ClientScopeRepresentation updateEntity(ClientScopeRepresentation entity, ContainerInfo node) { + entityResource(entity, node).update(entity); + return readEntity(entity, node); + } + + @Override + protected void deleteEntity(ClientScopeRepresentation testEntity, ContainerInfo node) { + entityResource(testEntity, node).remove(); + assertNull(readEntity(testEntity, node)); + } + + @Override + protected ClientScopeRepresentation testEntityUpdates(ClientScopeRepresentation testEntity, boolean backendFailover) { + // groupname + testEntity.setName("name-updated"); + testEntity = updateEntityOnCurrentFailNode(testEntity, "name"); + verifyEntityUpdateDuringFailover(testEntity, backendFailover); + + testEntity.setProtocol("protocol-updated"); + testEntity = updateEntityOnCurrentFailNode(testEntity, "protocol"); + verifyEntityUpdateDuringFailover(testEntity, backendFailover); + + testEntity.setDescription("description-updated"); + testEntity = updateEntityOnCurrentFailNode(testEntity, "description"); + verifyEntityUpdateDuringFailover(testEntity, backendFailover); + + testEntity.setAttributes(Map.of("updated", "updated")); + testEntity = updateEntityOnCurrentFailNode(testEntity, "attributes"); + verifyEntityUpdateDuringFailover(testEntity, backendFailover); + + return testEntity; + } + + private ClientScopesResource clientScopes(ContainerInfo node) { + return getAdminClientFor(node).realm(testRealmName).clientScopes(); + } +}