From a65fd34bbf49900287e68d0edbce7ba2e9a70d5a Mon Sep 17 00:00:00 2001 From: Pedro Ruivo Date: Wed, 27 Nov 2024 10:30:06 +0000 Subject: [PATCH] Make PermissionTicket events marshallable Fixes #35328 Signed-off-by: Pedro Ruivo --- .../marshalling/KeycloakModelSchema.java | 4 + .../org/keycloak/marshalling/Marshalling.java | 3 + .../events/BasePermissionTicketEvent.java | 93 +++++++ .../events/PermissionTicketRemovedEvent.java | 50 +--- .../events/PermissionTicketUpdatedEvent.java | 50 +--- ...rmissionTicketInvalidationClusterTest.java | 235 ++++++++++++++++++ 6 files changed, 355 insertions(+), 80 deletions(-) create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/BasePermissionTicketEvent.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/PermissionTicketInvalidationClusterTest.java diff --git a/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakModelSchema.java b/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakModelSchema.java index 4a16b97823e..2215f9ee499 100644 --- a/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakModelSchema.java +++ b/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakModelSchema.java @@ -36,6 +36,8 @@ import org.keycloak.keys.infinispan.PublicKeyStorageInvalidationEvent; import org.keycloak.models.UserSessionModel; import org.keycloak.models.cache.infinispan.ClearCacheEvent; +import org.keycloak.models.cache.infinispan.authorization.events.PermissionTicketRemovedEvent; +import org.keycloak.models.cache.infinispan.authorization.events.PermissionTicketUpdatedEvent; import org.keycloak.models.cache.infinispan.authorization.events.PolicyRemovedEvent; import org.keycloak.models.cache.infinispan.authorization.events.PolicyUpdatedEvent; import org.keycloak.models.cache.infinispan.authorization.events.ResourceRemovedEvent; @@ -136,6 +138,8 @@ ClearCacheEvent.class, //models.cache.infinispan.authorization.events package + PermissionTicketRemovedEvent.class, + PermissionTicketUpdatedEvent.class, PolicyUpdatedEvent.class, PolicyRemovedEvent.class, ResourceUpdatedEvent.class, diff --git a/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java b/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java index d2516757925..4db2b49e2be 100644 --- a/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java +++ b/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java @@ -160,6 +160,9 @@ private Marshalling() { public static final int MAP_ENTRY_TO_KEY_FUNCTION = 65611; public static final int SESSION_UNWRAP_MAPPER = 65612; + public static final int PERMISSION_TICKET_REMOVED_EVENT = 65613; + public static final int PERMISSION_TICKET_UPDATED_EVENT = 65614; + public static void configure(GlobalConfigurationBuilder builder) { builder.serialization() .addContextInitializer(KeycloakModelSchema.INSTANCE); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/BasePermissionTicketEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/BasePermissionTicketEvent.java new file mode 100644 index 00000000000..1af0cbccd69 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/BasePermissionTicketEvent.java @@ -0,0 +1,93 @@ +/* + * 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.models.cache.infinispan.authorization.events; + +import java.util.Objects; + +import org.infinispan.protostream.annotations.ProtoField; +import org.keycloak.models.cache.infinispan.events.InvalidationEvent; + +abstract class BasePermissionTicketEvent extends InvalidationEvent implements AuthorizationCacheInvalidationEvent { + + private final String owner; + private final String resource; + private final String scope; + private final String serverId; + private final String requester; + private final String resourceName; + + BasePermissionTicketEvent(String id, String owner, String resource, String scope, String serverId, String requester, String resourceName) { + super(id); + this.owner = owner; + this.resource = resource; + this.scope = scope; + this.serverId = serverId; + this.requester = requester; + this.resourceName = resourceName; + } + + @ProtoField(2) + public String getOwner() { + return owner; + } + + @ProtoField(3) + public String getRequester() { + return requester; + } + + @ProtoField(4) + public String getResource() { + return resource; + } + + @ProtoField(5) + public String getResourceName() { + return resourceName; + } + + @ProtoField(6) + public String getScope() { + return scope; + } + + @ProtoField(7) + public String getServerId() { + return serverId; + } + + @Override + public String toString() { + return "%s [ id=%s, name=%s]".formatted(getClass().getName(), getId(), resource); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + BasePermissionTicketEvent that = (BasePermissionTicketEvent) o; + return Objects.equals(resource, that.resource) && Objects.equals(serverId, that.serverId); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), resource, serverId); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java index 49e81eb8daf..7266fb91150 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java @@ -17,60 +17,30 @@ package org.keycloak.models.cache.infinispan.authorization.events; -import java.util.Objects; import java.util.Set; +import org.infinispan.protostream.annotations.ProtoFactory; +import org.infinispan.protostream.annotations.ProtoTypeId; +import org.keycloak.marshalling.Marshalling; import org.keycloak.models.cache.infinispan.authorization.StoreFactoryCacheManager; -import org.keycloak.models.cache.infinispan.events.InvalidationEvent; /** * @author Marek Posolda */ -public class PermissionTicketRemovedEvent extends InvalidationEvent implements AuthorizationCacheInvalidationEvent { +@ProtoTypeId(Marshalling.PERMISSION_TICKET_REMOVED_EVENT) +public class PermissionTicketRemovedEvent extends BasePermissionTicketEvent { - private String owner; - private String resource; - private String scope; - private String serverId; - private String requester; - private String resourceName; - - private PermissionTicketRemovedEvent(String id) { - super(id); + @ProtoFactory + PermissionTicketRemovedEvent(String id, String owner, String resource, String scope, String serverId, String requester, String resourceName) { + super(id, owner, resource, scope, serverId, requester, resourceName); } public static PermissionTicketRemovedEvent create(String id, String owner, String requester, String resource, String resourceName, String scope, String serverId) { - PermissionTicketRemovedEvent event = new PermissionTicketRemovedEvent(id); - event.owner = owner; - event.requester = requester; - event.resource = resource; - event.resourceName = resourceName; - event.scope = scope; - event.serverId = serverId; - return event; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - PermissionTicketRemovedEvent that = (PermissionTicketRemovedEvent) o; - return Objects.equals(resource, that.resource) && Objects.equals(serverId, that.serverId); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), resource, serverId); - } - - @Override - public String toString() { - return String.format("PermissionTicketRemovedEvent [ id=%s, name=%s]", getId(), resource); + return new PermissionTicketRemovedEvent(id, owner, resource, scope, serverId, requester, resourceName); } @Override public void addInvalidations(StoreFactoryCacheManager cache, Set invalidations) { - cache.permissionTicketRemoval(getId(), owner, requester, resource, resourceName, scope, serverId, invalidations); + cache.permissionTicketRemoval(getId(), getOwner(), getRequester(), getResource(), getResourceName(), getScope(), getServerId(), invalidations); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java index c3e6e74e193..8b6f04516b0 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java @@ -17,60 +17,30 @@ package org.keycloak.models.cache.infinispan.authorization.events; -import java.util.Objects; import java.util.Set; +import org.infinispan.protostream.annotations.ProtoFactory; +import org.infinispan.protostream.annotations.ProtoTypeId; +import org.keycloak.marshalling.Marshalling; import org.keycloak.models.cache.infinispan.authorization.StoreFactoryCacheManager; -import org.keycloak.models.cache.infinispan.events.InvalidationEvent; /** * @author Marek Posolda */ -public class PermissionTicketUpdatedEvent extends InvalidationEvent implements AuthorizationCacheInvalidationEvent { +@ProtoTypeId(Marshalling.PERMISSION_TICKET_UPDATED_EVENT) +public class PermissionTicketUpdatedEvent extends BasePermissionTicketEvent { - private String owner; - private String resource; - private String scope; - private String serverId; - private String requester; - private String resourceName; - - private PermissionTicketUpdatedEvent(String id) { - super(id); + @ProtoFactory + PermissionTicketUpdatedEvent(String id, String owner, String resource, String scope, String serverId, String requester, String resourceName) { + super(id, owner, resource, scope, serverId, requester, resourceName); } public static PermissionTicketUpdatedEvent create(String id, String owner, String requester, String resource, String resourceName, String scope, String serverId) { - PermissionTicketUpdatedEvent event = new PermissionTicketUpdatedEvent(id); - event.owner = owner; - event.requester = requester; - event.resource = resource; - event.resourceName = resourceName; - event.scope = scope; - event.serverId = serverId; - return event; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - PermissionTicketUpdatedEvent that = (PermissionTicketUpdatedEvent) o; - return Objects.equals(resource, that.resource) && Objects.equals(serverId, that.serverId); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), resource, serverId); - } - - @Override - public String toString() { - return String.format("PermissionTicketUpdatedEvent [ id=%s, name=%s]", getId(), resource); + return new PermissionTicketUpdatedEvent(id, owner, resource, scope, serverId, requester, resourceName); } @Override public void addInvalidations(StoreFactoryCacheManager cache, Set invalidations) { - cache.permissionTicketUpdated(getId(), owner, requester, resource, resourceName, scope, serverId, invalidations); + cache.permissionTicketUpdated(getId(), getOwner(), getRequester(), getResource(), getResourceName(), getScope(), getServerId(), invalidations); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/PermissionTicketInvalidationClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/PermissionTicketInvalidationClusterTest.java new file mode 100644 index 00000000000..7a4826dc598 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/PermissionTicketInvalidationClusterTest.java @@ -0,0 +1,235 @@ +/* + * 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.concurrent.ThreadLocalRandom; + +import org.apache.commons.lang.RandomStringUtils; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.AuthorizationProviderFactory; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.ContainerInfo; + +import static org.junit.Assert.assertEquals; + +public class PermissionTicketInvalidationClusterTest extends AbstractInvalidationClusterTestWithTestRealm { + + private String clientId; + private String userId; + private String resourceId; + private String scopeId; + private final String resourceName = RandomStringUtils.randomAlphabetic(5); + private final String scopeName = RandomStringUtils.randomAlphabetic(5); + + @Override + protected void createTestRealm(ContainerInfo node) { + super.createTestRealm(node); + createClient(node); + createUser(node); + createResource(node); + createScope(node); + } + + private void createClient(ContainerInfo node) { + var client = new ClientRepresentation(); + String s = RandomStringUtils.randomAlphabetic(5); + client.setClientId("client_" + s); + client.setName("name_" + s); + try (var rsp = getAdminClientFor(node) + .realm(testRealmName) + .clients() + .create(client)) { + clientId = ApiUtil.getCreatedId(rsp); + } + } + + private void createUser(ContainerInfo node) { + var user = createUserRepresentation("user1", "password"); + try (var rsp = getAdminClientFor(node) + .realm(testRealmName) + .users() + .create(user)) { + userId = ApiUtil.getCreatedId(rsp); + } + } + + private void createResource(ContainerInfo node) { + var realmFinal = testRealmName; + var clientFinal = clientId; + var resourceFinal = resourceName; + resourceId = getTestingClientFor(node).server().fetchString(session -> { + var realm = session.realms().getRealmByName(realmFinal); + session.getContext().setRealm(realm); + var factory = (AuthorizationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(AuthorizationProvider.class); + var storeFactory = factory.create(session, realm).getStoreFactory(); + var client = session.clients().getClientById(realm, clientFinal); + + var resourceServer = storeFactory.getResourceServerStore().findByClient(client); + if (resourceServer == null) { + resourceServer = storeFactory.getResourceServerStore().create(client); + } + return storeFactory.getResourceStore().create(resourceServer, resourceFinal, clientFinal).getId(); + }).replaceAll("\"", ""); + } + + private void createScope(ContainerInfo node) { + var realmFinal = testRealmName; + var clientFinal = clientId; + var scopeFinal = scopeName; + scopeId = getTestingClientFor(node).server().fetchString(session -> { + var realm = session.realms().getRealmByName(realmFinal); + session.getContext().setRealm(realm); + var storeFactory = session.getProvider(AuthorizationProvider.class).getStoreFactory(); + var client = session.clients().getClientById(realm, clientFinal); + + var resourceServer = storeFactory.getResourceServerStore().findByClient(client); + if (resourceServer == null) { + resourceServer = storeFactory.getResourceServerStore().create(client); + } + return storeFactory.getScopeStore().create(resourceServer, scopeFinal).getId(); + }).replaceAll("\"", ""); + } + + @Override + protected PermissionTicketRepresentation createTestEntityRepresentation() { + var ticket = new PermissionTicketRepresentation(); + ticket.setGranted(true); + ticket.setOwner(clientId); // client is the owner + ticket.setRequester(userId); // userid is the requester + ticket.setScope(scopeId); + ticket.setResource(resourceId); + return ticket; + } + + @Override + protected Object entityResource(PermissionTicketRepresentation testEntity, ContainerInfo node) { + throw new UnsupportedOperationException(); + } + + @Override + protected Object entityResource(String idOrName, ContainerInfo node) { + throw new UnsupportedOperationException(); + } + + @Override + protected PermissionTicketRepresentation createEntity(PermissionTicketRepresentation testEntity, ContainerInfo node) { + var realmFinal = testRealmName; + var clientFinal = clientId; + var scopeFinal = scopeId; + var resourceFinal = resourceId; + var userFinal = userId; + return getTestingClientFor(node).server().fetch(session -> { + var realm = session.realms().getRealmByName(realmFinal); + session.getContext().setRealm(realm); + var factory = (AuthorizationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(AuthorizationProvider.class); + var provider = factory.create(session, realm); + var storeFactory = provider.getStoreFactory(); + var client = session.clients().getClientById(realm, clientFinal); + + var resourceServer = storeFactory.getResourceServerStore().findByClient(client); + var resource = storeFactory.getResourceStore().findById(resourceServer, resourceFinal); + var scope = storeFactory.getScopeStore().findById(resourceServer, scopeFinal); + var ticket = storeFactory.getPermissionTicketStore().create(resourceServer, resource, scope, userFinal); + return ModelToRepresentation.toRepresentation(ticket, provider, false); + }, PermissionTicketRepresentation.class); + } + + @Override + protected PermissionTicketRepresentation readEntity(PermissionTicketRepresentation entity, ContainerInfo node) { + var realmFinal = testRealmName; + var clientFinal = clientId; + var idFinal = entity.getId(); + return getTestingClientFor(node).server().fetch(session -> { + var realm = session.realms().getRealmByName(realmFinal); + session.getContext().setRealm(realm); + var factory = (AuthorizationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(AuthorizationProvider.class); + var provider = factory.create(session, realm); + var storeFactory = provider.getStoreFactory(); + var client = session.clients().getClientById(realm, clientFinal); + + var resourceServer = storeFactory.getResourceServerStore().findByClient(client); + var ticket = storeFactory.getPermissionTicketStore().findById(resourceServer, idFinal); + return ticket == null ? null : ModelToRepresentation.toRepresentation(ticket, provider, false); + }, PermissionTicketRepresentation.class); + } + + @Override + protected PermissionTicketRepresentation updateEntity(PermissionTicketRepresentation entity, ContainerInfo node) { + throw new UnsupportedOperationException(); + } + + @Override + protected void deleteEntity(PermissionTicketRepresentation entity, ContainerInfo node) { + var realmFinal = testRealmName; + var idFinal = entity.getId(); + getTestingClientFor(node).server().run(session -> { + var realm = session.realms().getRealmByName(realmFinal); + session.getContext().setRealm(realm); + var factory = (AuthorizationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(AuthorizationProvider.class); + var storeFactory = factory.create(session, realm).getStoreFactory(); + storeFactory.getPermissionTicketStore().delete(idFinal); + }); + } + + @Override + protected PermissionTicketRepresentation testEntityUpdates(PermissionTicketRepresentation entity, boolean backendFailover) { + final long timestamp = ThreadLocalRandom.current().nextLong(100000); + var realmFinal = testRealmName; + var clientFinal = clientId; + var idFinal = entity.getId(); + + getTestingClientFor(getCurrentFailNode()).server().run(session -> { + var realm = session.realms().getRealmByName(realmFinal); + session.getContext().setRealm(realm); + var factory = (AuthorizationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(AuthorizationProvider.class); + var storeFactory = factory.create(session, realm).getStoreFactory(); + var client = session.clients().getClientById(realm, clientFinal); + + var resourceServer = storeFactory.getResourceServerStore().findByClient(client); + var ticket = storeFactory.getPermissionTicketStore().findById(resourceServer, idFinal); + ticket.setGrantedTimestamp(timestamp); + }); + + if (backendFailover) { + failure(); + } + + for (var node : getCurrentSurvivorNodes()) { + var rsp = getTestingClientFor(node).server().fetchString(session -> { + var realm = session.realms().getRealmByName(realmFinal); + session.getContext().setRealm(realm); + var factory = (AuthorizationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(AuthorizationProvider.class); + var storeFactory = factory.create(session, realm).getStoreFactory(); + var client = session.clients().getClientById(realm, clientFinal); + + var resourceServer = storeFactory.getResourceServerStore().findByClient(client); + var ticket = storeFactory.getPermissionTicketStore().findById(resourceServer, idFinal); + return Long.toString(ticket.getGrantedTimestamp()); + }); + assertEquals(timestamp, Long.parseLong(rsp.replaceAll("\"", ""))); + } + + failback(); + iterateCurrentFailNode(); + + return entity; + } +}