From a24abe245bed1669d0345e596a5e9c8e143d09a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 17 Sep 2020 15:31:47 +0200 Subject: [PATCH] Invalidate authorization cache when namespace permissions are changed --- .../sonia/scm/repository/NamespaceEvent.java | 47 ++++++++++++++++ .../NamespaceModificationEvent.java | 51 ++++++++++++++++++ .../repository/DefaultNamespaceManager.java | 16 +++++- .../AuthorizationChangedEventProducer.java | 43 ++++++++++++--- .../DefaultNamespaceManagerTest.java | 11 +++- ...AuthorizationChangedEventProducerTest.java | 53 ++++++++++++++++++- 6 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/NamespaceEvent.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/NamespaceModificationEvent.java diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceEvent.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceEvent.java new file mode 100644 index 0000000000..8276d81aad --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceEvent.java @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.repository; + + +import sonia.scm.HandlerEventType; +import sonia.scm.event.AbstractHandlerEvent; +import sonia.scm.event.Event; + +/** + * The NamespaceEvent is fired if a {@link Namespace} object changes. + * + * @since 2.6.0 + */ +@Event +public class NamespaceEvent extends AbstractHandlerEvent { + + public NamespaceEvent(HandlerEventType eventType, Namespace namespace) { + super(eventType, namespace); + } + + public NamespaceEvent(HandlerEventType eventType, Namespace namespace, Namespace oldNamespace) { + super(eventType, namespace, oldNamespace); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceModificationEvent.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceModificationEvent.java new file mode 100644 index 0000000000..e7bd25de1a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceModificationEvent.java @@ -0,0 +1,51 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.repository; + +import sonia.scm.HandlerEventType; +import sonia.scm.ModificationHandlerEvent; +import sonia.scm.event.Event; + +/** + * Event which is fired whenever a namespace is modified. + * + * @since 2.6.0 + */ +@Event +public final class NamespaceModificationEvent extends NamespaceEvent implements ModificationHandlerEvent { + + private final Namespace itemBeforeModification; + + public NamespaceModificationEvent(HandlerEventType eventType, Namespace item, Namespace itemBeforeModification) { + super(eventType, item, itemBeforeModification); + this.itemBeforeModification = itemBeforeModification; + } + + @Override + public Namespace getItemBeforeModification() { + return itemBeforeModification; + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultNamespaceManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultNamespaceManager.java index e0d6ec9117..aa3abd4211 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultNamespaceManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultNamespaceManager.java @@ -24,6 +24,10 @@ package sonia.scm.repository; +import com.github.legman.EventBus; +import sonia.scm.HandlerEventType; +import sonia.scm.event.ScmEventBus; + import javax.inject.Inject; import java.util.Collection; import java.util.Optional; @@ -36,11 +40,13 @@ public class DefaultNamespaceManager implements NamespaceManager { private final RepositoryManager repositoryManager; private final NamespaceDao dao; + private final EventBus eventBus; @Inject - public DefaultNamespaceManager(RepositoryManager repositoryManager, NamespaceDao dao) { + public DefaultNamespaceManager(RepositoryManager repositoryManager, NamespaceDao dao, EventBus eventBus) { this.repositoryManager = repositoryManager; this.dao = dao; + this.eventBus = eventBus; } @Override @@ -64,10 +70,14 @@ public class DefaultNamespaceManager implements NamespaceManager { @Override public void modify(Namespace namespace) { + Namespace oldNamespace = get(namespace.getNamespace()) + .orElseThrow(() -> notFound(entity(Namespace.class, namespace.getNamespace()))); + fireEvent(HandlerEventType.BEFORE_MODIFY, namespace, oldNamespace); if (!get(namespace.getNamespace()).isPresent()) { throw notFound(entity("Namespace", namespace.getNamespace())); } dao.add(namespace); + fireEvent(HandlerEventType.MODIFY, namespace, oldNamespace); } private Namespace createNamespaceForName(String namespace) { @@ -75,4 +85,8 @@ public class DefaultNamespaceManager implements NamespaceManager { .map(Namespace::clone) .orElse(new Namespace(namespace)); } + + protected void fireEvent(HandlerEventType event, Namespace namespace, Namespace oldNamespace) { + eventBus.post(new NamespaceModificationEvent(event, namespace, oldNamespace)); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java index a198dafb24..2b3b1b0fcc 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java +++ b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.security; import com.github.legman.Subscribe; @@ -35,14 +35,19 @@ import sonia.scm.event.ScmEventBus; import sonia.scm.group.Group; import sonia.scm.group.GroupEvent; import sonia.scm.group.GroupModificationEvent; +import sonia.scm.repository.Namespace; +import sonia.scm.repository.NamespaceEvent; +import sonia.scm.repository.NamespaceModificationEvent; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryEvent; import sonia.scm.repository.RepositoryModificationEvent; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.user.User; import sonia.scm.user.UserEvent; import sonia.scm.user.UserModificationEvent; import javax.inject.Singleton; +import java.util.Collection; /** * Receives all kinds of events, which affects authorization relevant data and fires an @@ -146,23 +151,47 @@ public class AuthorizationChangedEventProducer { } } + @Subscribe + public void onEvent(NamespaceEvent event) { + if (event.getEventType().isPost()) { + if (isModificationEvent(event)) { + handleNamespaceModificationEvent((NamespaceModificationEvent) event); + } + } + } + private void handleRepositoryModificationEvent(RepositoryModificationEvent event) { Repository repository = event.getItem(); - if (isAuthorizationDataModified(repository, event.getItemBeforeModification())) { + if (isAuthorizationDataModified(repository.getPermissions(), event.getItemBeforeModification().getPermissions())) { logger.debug( - "fire authorization changed event, because a relevant field of repository {} has changed", repository.getName() + "fire authorization changed event, because a relevant field of repository {}/{} has changed", repository.getNamespace(), repository.getName() ); fireEventForEveryUser(); } else { logger.debug( - "authorization changed event is not fired, because non relevant field of repository {} has changed", - repository.getName() + "authorization changed event is not fired, because non relevant field of repository {}/{} has changed", + repository.getNamespace(), repository.getName() ); } } - private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) { - return !(repository.getPermissions().containsAll(beforeModification.getPermissions()) && beforeModification.getPermissions().containsAll(repository.getPermissions())); + private void handleNamespaceModificationEvent(NamespaceModificationEvent event) { + Namespace namespace = event.getItem(); + if (isAuthorizationDataModified(namespace.getPermissions(), event.getItemBeforeModification().getPermissions())) { + logger.debug( + "fire authorization changed event, because a relevant field of namespace {} has changed", namespace.getNamespace() + ); + fireEventForEveryUser(); + } else { + logger.debug( + "authorization changed event is not fired, because non relevant field of namespace {} has changed", + namespace.getNamespace() + ); + } + } + + private boolean isAuthorizationDataModified(Collection newPermissions, Collection permissionsBeforeModification) { + return !(newPermissions.containsAll(permissionsBeforeModification) && permissionsBeforeModification.containsAll(newPermissions)); } private void fireEventForEveryUser() { diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultNamespaceManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultNamespaceManagerTest.java index 736e1a90d1..81c0b22cdb 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultNamespaceManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultNamespaceManagerTest.java @@ -24,11 +24,13 @@ package sonia.scm.repository; +import com.github.legman.EventBus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.HandlerEventType; import sonia.scm.store.InMemoryDataStore; import sonia.scm.store.InMemoryDataStoreFactory; @@ -37,6 +39,9 @@ import java.util.Optional; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,6 +50,8 @@ class DefaultNamespaceManagerTest { @Mock RepositoryManager repositoryManager; + @Mock + EventBus eventBus; Namespace life; @@ -56,7 +63,7 @@ class DefaultNamespaceManagerTest { @BeforeEach void mockExistingNamespaces() { dao = new NamespaceDao(new InMemoryDataStoreFactory(new InMemoryDataStore())); - manager = new DefaultNamespaceManager(repositoryManager, dao); + manager = new DefaultNamespaceManager(repositoryManager, dao, eventBus); when(repositoryManager.getAllNamespaces()).thenReturn(asList("life", "universe", "rest")); @@ -115,5 +122,7 @@ class DefaultNamespaceManagerTest { Namespace newLife = manager.get("life").get(); assertThat(newLife).isEqualTo(modifiedNamespace); + verify(eventBus).post(argThat(event -> ((NamespaceModificationEvent)event).getEventType() == HandlerEventType.BEFORE_MODIFY)); + verify(eventBus).post(argThat(event -> ((NamespaceModificationEvent)event).getEventType() == HandlerEventType.MODIFY)); } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java index 30aa0d8091..ae5405de50 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.security; import com.google.common.collect.Lists; @@ -31,6 +31,8 @@ import sonia.scm.HandlerEventType; import sonia.scm.group.Group; import sonia.scm.group.GroupEvent; import sonia.scm.group.GroupModificationEvent; +import sonia.scm.repository.Namespace; +import sonia.scm.repository.NamespaceModificationEvent; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryEvent; import sonia.scm.repository.RepositoryModificationEvent; @@ -251,6 +253,55 @@ public class AuthorizationChangedEventProducerTest { assertUserEventIsFired("trillian"); } + @Test + public void testOnNamespaceModificationEvent() + { + Namespace namespaceModified = new Namespace("hitchhiker"); + namespaceModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false))); + + Namespace namespace = new Namespace("hitchhiker"); + namespace.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false))); + + producer.onEvent(new NamespaceModificationEvent(HandlerEventType.BEFORE_CREATE, namespaceModified, namespace)); + assertEventIsNotFired(); + + producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace)); + assertEventIsNotFired(); + + namespaceModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false))); + producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace)); + assertEventIsNotFired(); + + namespaceModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test123", singletonList("read"), false))); + producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace)); + assertGlobalEventIsFired(); + + resetStoredEvent(); + + namespaceModified.setPermissions( + Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), true)) + ); + producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace)); + assertGlobalEventIsFired(); + + resetStoredEvent(); + + namespaceModified.setPermissions( + Lists.newArrayList(new RepositoryPermission("test", asList("read", "write"), false)) + ); + producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace)); + assertGlobalEventIsFired(); + + resetStoredEvent(); + namespace.setPermissions(Lists.newArrayList(new RepositoryPermission("test", asList("read", "write"), false))); + + namespaceModified.setPermissions( + Lists.newArrayList(new RepositoryPermission("test", asList("write", "read"), false)) + ); + producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace)); + assertEventIsNotFired(); + } + private static class StoringAuthorizationChangedEventProducer extends AuthorizationChangedEventProducer { private AuthorizationChangedEvent event;