diff --git a/gradle/changelog/privileged_startup_api.yaml b/gradle/changelog/privileged_startup_api.yaml new file mode 100644 index 0000000000..2733146490 --- /dev/null +++ b/gradle/changelog/privileged_startup_api.yaml @@ -0,0 +1,2 @@ +- type: fixed + description: Split SetupContextListener logic into new Privileged Startup API ([#1573](https://github.com/scm-manager/scm-manager/pull/1573)) diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/AdminAccountStartupAction.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/AdminAccountStartupAction.java new file mode 100644 index 0000000000..b01070bd52 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/AdminAccountStartupAction.java @@ -0,0 +1,76 @@ +/* + * 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.lifecycle; + +import org.apache.shiro.authc.credential.PasswordService; +import sonia.scm.SCMContext; +import sonia.scm.plugin.Extension; +import sonia.scm.security.PermissionAssigner; +import sonia.scm.security.PermissionDescriptor; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; + +import javax.inject.Inject; +import java.util.Collections; + +@Extension +public class AdminAccountStartupAction implements PrivilegedStartupAction { + + private final PasswordService passwordService; + private final UserManager userManager; + private final PermissionAssigner permissionAssigner; + + @Inject + public AdminAccountStartupAction(PasswordService passwordService, UserManager userManager, PermissionAssigner permissionAssigner) { + this.passwordService = passwordService; + this.userManager = userManager; + this.permissionAssigner = permissionAssigner; + } + + @Override + public void run() { + if (shouldCreateAdminAccount()) { + createAdminAccount(); + } + } + + private void createAdminAccount() { + User scmadmin = new User("scmadmin", "SCM Administrator", "scm-admin@scm-manager.org"); + String password = passwordService.encryptPassword("scmadmin"); + scmadmin.setPassword(password); + userManager.create(scmadmin); + + PermissionDescriptor descriptor = new PermissionDescriptor("*"); + permissionAssigner.setPermissionsForUser("scmadmin", Collections.singleton(descriptor)); + } + + private boolean shouldCreateAdminAccount() { + return !Boolean.getBoolean("sonia.scm.skipAdminCreation") && (userManager.getAll().isEmpty() || onlyAnonymousUserExists()); + } + + private boolean onlyAnonymousUserExists() { + return userManager.getAll().size() == 1 && userManager.contains(SCMContext.USER_ANONYMOUS); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/AnonymousUserStartupAction.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/AnonymousUserStartupAction.java new file mode 100644 index 0000000000..b138c9c156 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/AnonymousUserStartupAction.java @@ -0,0 +1,57 @@ +/* + * 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.lifecycle; + +import sonia.scm.SCMContext; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.security.AnonymousMode; +import sonia.scm.user.UserManager; + +import javax.inject.Inject; + +@Extension +public class AnonymousUserStartupAction implements PrivilegedStartupAction { + + private final ScmConfiguration scmConfiguration; + private final UserManager userManager; + + @Inject + public AnonymousUserStartupAction(ScmConfiguration scmConfiguration, UserManager userManager) { + this.scmConfiguration = scmConfiguration; + this.userManager = userManager; + } + + @Override + public void run() { + if (anonymousUserRequiredButNotExists()) { + userManager.create(SCMContext.ANONYMOUS); + } + } + + private boolean anonymousUserRequiredButNotExists() { + return scmConfiguration.getAnonymousMode() != AnonymousMode.OFF && !userManager.contains(SCMContext.USER_ANONYMOUS); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/AuthenticatedGroupStartupAction.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/AuthenticatedGroupStartupAction.java new file mode 100644 index 0000000000..a5fb10d39a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/AuthenticatedGroupStartupAction.java @@ -0,0 +1,66 @@ +/* + * 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.lifecycle; + +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.group.Group; +import sonia.scm.group.GroupManager; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; + +import static sonia.scm.group.GroupCollector.AUTHENTICATED; + +@Extension +public class AuthenticatedGroupStartupAction implements PrivilegedStartupAction { + + @VisibleForTesting + static final String AUTHENTICATED_GROUP_DESCRIPTION = "Includes all authenticated users"; + + private final GroupManager groupManager; + + @Inject + public AuthenticatedGroupStartupAction(GroupManager groupManager) { + this.groupManager = groupManager; + } + + @Override + public void run() { + if (authenticatedGroupDoesNotExists()) { + createAuthenticatedGroup(); + } + } + + private boolean authenticatedGroupDoesNotExists() { + return groupManager.get(AUTHENTICATED) == null; + } + + private void createAuthenticatedGroup() { + Group authenticated = new Group("xml", AUTHENTICATED); + authenticated.setDescription(AUTHENTICATED_GROUP_DESCRIPTION); + authenticated.setExternal(true); + groupManager.create(authenticated); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/PrivilegedStartupAction.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/PrivilegedStartupAction.java new file mode 100644 index 0000000000..f2344393d0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/PrivilegedStartupAction.java @@ -0,0 +1,31 @@ +/* + * 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.lifecycle; + +import sonia.scm.plugin.ExtensionPoint; +import sonia.scm.web.security.PrivilegedAction; + +@ExtensionPoint +interface PrivilegedStartupAction extends PrivilegedAction {} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/RepositoryExportCleanupStartupAction.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/RepositoryExportCleanupStartupAction.java new file mode 100644 index 0000000000..c121d0e082 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/RepositoryExportCleanupStartupAction.java @@ -0,0 +1,46 @@ +/* + * 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.lifecycle; + +import sonia.scm.importexport.ExportService; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; + +@Extension +public class RepositoryExportCleanupStartupAction implements PrivilegedStartupAction { + + private final ExportService exportService; + + @Inject + public RepositoryExportCleanupStartupAction(ExportService exportService) { + this.exportService = exportService; + } + + @Override + public void run() { + exportService.cleanupUnfinishedExports(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java index f604a9e25e..130820f4fa 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java @@ -21,128 +21,35 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.lifecycle; -import com.google.common.annotations.VisibleForTesting; -import org.apache.shiro.authc.credential.PasswordService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.SCMContext; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.group.Group; -import sonia.scm.group.GroupManager; -import sonia.scm.importexport.ExportService; import sonia.scm.plugin.Extension; -import sonia.scm.security.AnonymousMode; -import sonia.scm.security.PermissionAssigner; -import sonia.scm.security.PermissionDescriptor; -import sonia.scm.user.User; -import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; -import sonia.scm.web.security.PrivilegedAction; import javax.inject.Inject; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; -import java.util.Collections; - -import static sonia.scm.group.GroupCollector.AUTHENTICATED; +import java.util.Set; @Extension public class SetupContextListener implements ServletContextListener { - private static final Logger LOG = LoggerFactory.getLogger(SetupContextListener.class); - + private final Set startupActions; private final AdministrationContext administrationContext; @Inject - public SetupContextListener(AdministrationContext administrationContext) { + public SetupContextListener(Set startupActions, AdministrationContext administrationContext) { + this.startupActions = startupActions; this.administrationContext = administrationContext; } @Override public void contextInitialized(ServletContextEvent sce) { - if (Boolean.getBoolean("sonia.scm.skipAdminCreation")) { - LOG.info("found skipAdminCreation flag; skipping creation of scmadmin"); - } else { - administrationContext.runAsAdmin(SetupAction.class); - } + startupActions.forEach(administrationContext::runAsAdmin); } @Override - public void contextDestroyed(ServletContextEvent sce) {} - - @VisibleForTesting - static class SetupAction implements PrivilegedAction { - - private final UserManager userManager; - private final PasswordService passwordService; - private final PermissionAssigner permissionAssigner; - private final ScmConfiguration scmConfiguration; - private final GroupManager groupManager; - private final ExportService exportService; - - @VisibleForTesting - static final String AUTHENTICATED_GROUP_DESCRIPTION = "Includes all authenticated users"; - - @Inject - public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner, ScmConfiguration scmConfiguration, GroupManager groupManager, ExportService exportService) { - this.userManager = userManager; - this.passwordService = passwordService; - this.permissionAssigner = permissionAssigner; - this.scmConfiguration = scmConfiguration; - this.groupManager = groupManager; - this.exportService = exportService; - } - - @Override - public void run() { - if (shouldCreateAdminAccount()) { - createAdminAccount(); - } - if (anonymousUserRequiredButNotExists()) { - userManager.create(SCMContext.ANONYMOUS); - } - - if (authenticatedGroupDoesNotExists()) { - createAuthenticatedGroup(); - } - - exportService.cleanupUnfinishedExports(); - } - - private boolean anonymousUserRequiredButNotExists() { - return scmConfiguration.getAnonymousMode() != AnonymousMode.OFF && !userManager.contains(SCMContext.USER_ANONYMOUS); - } - - private boolean shouldCreateAdminAccount() { - return userManager.getAll().isEmpty() || onlyAnonymousUserExists(); - } - - private boolean onlyAnonymousUserExists() { - return userManager.getAll().size() == 1 && userManager.contains(SCMContext.USER_ANONYMOUS); - } - - private void createAdminAccount() { - User scmadmin = new User("scmadmin", "SCM Administrator", "scm-admin@scm-manager.org"); - String password = passwordService.encryptPassword("scmadmin"); - scmadmin.setPassword(password); - userManager.create(scmadmin); - - PermissionDescriptor descriptor = new PermissionDescriptor("*"); - permissionAssigner.setPermissionsForUser("scmadmin", Collections.singleton(descriptor)); - } - - private boolean authenticatedGroupDoesNotExists() { - return groupManager.get(AUTHENTICATED) == null; - } - - private void createAuthenticatedGroup() { - Group authenticated = new Group("xml", AUTHENTICATED); - authenticated.setDescription(AUTHENTICATED_GROUP_DESCRIPTION); - authenticated.setExternal(true); - groupManager.create(authenticated); - } + public void contextDestroyed(ServletContextEvent sce) { } } diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/AdminAccountStartupActionTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/AdminAccountStartupActionTest.java new file mode 100644 index 0000000000..47f1a09aea --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/AdminAccountStartupActionTest.java @@ -0,0 +1,132 @@ +/* + * 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.lifecycle; + +import com.google.common.collect.Lists; +import org.apache.shiro.authc.credential.PasswordService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import sonia.scm.SCMContext; +import sonia.scm.security.PermissionAssigner; +import sonia.scm.security.PermissionDescriptor; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; +import sonia.scm.user.UserTestData; + +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AdminAccountStartupActionTest { + + @Mock + private PasswordService passwordService; + @Mock + private UserManager userManager; + @Mock + private PermissionAssigner permissionAssigner; + + @InjectMocks + private AdminAccountStartupAction startupAction; + + @Test + void shouldCreateAdminAccountIfNoUserExistsAndAssignPermissions() { + when(passwordService.encryptPassword("scmadmin")).thenReturn("secret"); + + startupAction.run(); + + verifyAdminCreated(); + verifyAdminPermissionsAssigned(); + } + + @Test + void shouldCreateAdminAccountIfOnlyAnonymousUserExistsAndAssignPermissions() { + when(userManager.getAll()).thenReturn(Lists.newArrayList(SCMContext.ANONYMOUS)); + when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(true); + when(passwordService.encryptPassword("scmadmin")).thenReturn("secret"); + + startupAction.run(); + + verifyAdminCreated(); + verifyAdminPermissionsAssigned(); + } + + @Test + @MockitoSettings(strictness = Strictness.LENIENT) + void shouldSkipAdminAccountCreationIfPropertyIsSet() { + System.setProperty("sonia.scm.skipAdminCreation", "true"); + + try { + startupAction.run(); + + verify(userManager, never()).create(any()); + verify(permissionAssigner, never()).setPermissionsForUser(anyString(), any(Collection.class)); + } finally { + System.setProperty("sonia.scm.skipAdminCreation", ""); + } + } + + @Test + void shouldDoNothingOnSecondStart() { + List users = Lists.newArrayList(UserTestData.createTrillian()); + when(userManager.getAll()).thenReturn(users); + + startupAction.run(); + + verify(userManager, never()).create(any(User.class)); + verify(permissionAssigner, never()).setPermissionsForUser(anyString(), any(Collection.class)); + } + + private void verifyAdminPermissionsAssigned() { + ArgumentCaptor usernameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor> permissionCaptor = ArgumentCaptor.forClass(Collection.class); + verify(permissionAssigner).setPermissionsForUser(usernameCaptor.capture(), permissionCaptor.capture()); + String username = usernameCaptor.getValue(); + assertThat(username).isEqualTo("scmadmin"); + PermissionDescriptor descriptor = permissionCaptor.getValue().iterator().next(); + assertThat(descriptor.getValue()).isEqualTo("*"); + } + + private void verifyAdminCreated() { + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(userManager).create(userCaptor.capture()); + User user = userCaptor.getValue(); + assertThat(user.getName()).isEqualTo("scmadmin"); + assertThat(user.getPassword()).isEqualTo("secret"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/AnonymousUserStartupActionTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/AnonymousUserStartupActionTest.java new file mode 100644 index 0000000000..29f996d4ec --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/AnonymousUserStartupActionTest.java @@ -0,0 +1,90 @@ +/* + * 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.lifecycle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContext; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousMode; +import sonia.scm.user.UserManager; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AnonymousUserStartupActionTest { + + @Mock + private ScmConfiguration scmConfiguration; + @Mock + private UserManager userManager; + + @InjectMocks + private AnonymousUserStartupAction startupAction; + + @Test + void shouldCreateAnonymousUserIfFullModeEnabled() { + when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(false); + when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL); + + startupAction.run(); + + verify(userManager).create(SCMContext.ANONYMOUS); + } + + @Test + void shouldCreateAnonymousUserIfProtocolModeEnabled() { + when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(false); + when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.PROTOCOL_ONLY); + + startupAction.run(); + + verify(userManager).create(SCMContext.ANONYMOUS); + } + + @Test + void shouldNotCreateAnonymousUserIfNotRequired() { + when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.OFF); + + startupAction.run(); + + verify(userManager, never()).create(SCMContext.ANONYMOUS); + } + + @Test + void shouldNotCreateAnonymousUserIfAlreadyExists() { + when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(true); + when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL); + + startupAction.run(); + + verify(userManager, never()).create(SCMContext.ANONYMOUS); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/AuthenticatedGroupStartupActionTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/AuthenticatedGroupStartupActionTest.java new file mode 100644 index 0000000000..d3af60ecd0 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/AuthenticatedGroupStartupActionTest.java @@ -0,0 +1,77 @@ +/* + * 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.lifecycle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.group.Group; +import sonia.scm.group.GroupManager; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static sonia.scm.group.GroupCollector.AUTHENTICATED; +import static sonia.scm.lifecycle.AuthenticatedGroupStartupAction.AUTHENTICATED_GROUP_DESCRIPTION; + +@ExtendWith(MockitoExtension.class) +class AuthenticatedGroupStartupActionTest { + + @Mock + private GroupManager groupManager; + + @InjectMocks + private AuthenticatedGroupStartupAction startupAction; + + @Test + void shouldCreateAuthenticatedGroupIfMissing() { + when(groupManager.get(AUTHENTICATED)).thenReturn(null); + + startupAction.run(); + + Group authenticated = createAuthenticatedGroup(); + authenticated.setDescription(AUTHENTICATED_GROUP_DESCRIPTION); + authenticated.setExternal(true); + + verify(groupManager, times(1)).create(authenticated); + } + + @Test + void shouldNotCreateAuthenticatedGroupIfAlreadyExists() { + when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup()); + + startupAction.run(); + + verify(groupManager, never()).create(any()); + } + + private Group createAuthenticatedGroup() { + return new Group("xml", AUTHENTICATED); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/RepositoryExportCleanupStartupActionTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/RepositoryExportCleanupStartupActionTest.java new file mode 100644 index 0000000000..2a86ada8c3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/RepositoryExportCleanupStartupActionTest.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.lifecycle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.importexport.ExportService; + +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class RepositoryExportCleanupStartupActionTest { + + @Mock + private ExportService exportService; + + @InjectMocks + private RepositoryExportCleanupStartupAction startupAction; + + @Test + void shouldCleanupUnfinishedRepositoryExports() { + startupAction.run(); + + verify(exportService).cleanupUnfinishedExports(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java index 4dfe366dd7..d8bf8131a3 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java @@ -24,43 +24,21 @@ package sonia.scm.lifecycle; -import com.google.common.collect.Lists; -import org.apache.shiro.authc.credential.PasswordService; +import com.google.common.collect.ImmutableSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import sonia.scm.SCMContext; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.group.Group; -import sonia.scm.group.GroupManager; -import sonia.scm.importexport.ExportService; -import sonia.scm.security.AnonymousMode; -import sonia.scm.security.PermissionAssigner; -import sonia.scm.security.PermissionDescriptor; -import sonia.scm.user.User; -import sonia.scm.user.UserManager; -import sonia.scm.user.UserTestData; import sonia.scm.web.security.AdministrationContext; -import java.util.Collection; -import java.util.List; +import javax.servlet.ServletContextEvent; +import java.util.Set; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static sonia.scm.group.GroupCollector.AUTHENTICATED; -import static sonia.scm.lifecycle.SetupContextListener.SetupAction.AUTHENTICATED_GROUP_DESCRIPTION; @ExtendWith(MockitoExtension.class) class SetupContextListenerTest { @@ -68,177 +46,20 @@ class SetupContextListenerTest { @Mock private AdministrationContext administrationContext; - @InjectMocks private SetupContextListener setupContextListener; - @Mock - private UserManager userManager; - - @Mock - private PasswordService passwordService; - - @Mock - ScmConfiguration scmConfiguration; - - @Mock - private GroupManager groupManager; - - @Mock - private ExportService exportService; - - @Mock - private PermissionAssigner permissionAssigner; - - @InjectMocks - private SetupContextListener.SetupAction setupAction; - @BeforeEach - void mockScmConfiguration() { - when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.OFF); - } - - @BeforeEach - void setupObjectUnderTest() { - doAnswer(ic -> { - setupAction.run(); - return null; - }).when(administrationContext).runAsAdmin(SetupContextListener.SetupAction.class); + void initSetupContextListener() { + Set startupActions = ImmutableSet.of(() -> {}, () -> {}); + setupContextListener = new SetupContextListener(startupActions, administrationContext); } @Test - void shouldCreateAdminAccountIfNoUserExistsAndAssignPermissions() { - when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup()); - when(passwordService.encryptPassword("scmadmin")).thenReturn("secret"); + void shouldRunStartupActionsWithAdministrationContext() { + ServletContextEvent contextEvent = mock(ServletContextEvent.class); - setupContextListener.contextInitialized(null); + setupContextListener.contextInitialized(contextEvent); - verifyAdminCreated(); - verifyAdminPermissionsAssigned(); - } - - @Test - void shouldCreateAdminAccountIfOnlyAnonymousUserExistsAndAssignPermissions() { - when(userManager.getAll()).thenReturn(Lists.newArrayList(SCMContext.ANONYMOUS)); - when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(true); - when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup()); - when(passwordService.encryptPassword("scmadmin")).thenReturn("secret"); - - setupContextListener.contextInitialized(null); - - verifyAdminCreated(); - verifyAdminPermissionsAssigned(); - } - - @Test - @MockitoSettings(strictness = Strictness.LENIENT) - void shouldSkipAdminAccountCreationIfPropertyIsSet() { - System.setProperty("sonia.scm.skipAdminCreation", "true"); - - try { - setupContextListener.contextInitialized(null); - - verify(userManager, never()).create(any()); - verify(permissionAssigner, never()).setPermissionsForUser(anyString(), any(Collection.class)); - } finally { - System.setProperty("sonia.scm.skipAdminCreation", ""); - } - } - - @Test - void shouldDoNothingOnSecondStart() { - List users = Lists.newArrayList(UserTestData.createTrillian()); - when(userManager.getAll()).thenReturn(users); - when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup()); - - setupContextListener.contextInitialized(null); - - verify(userManager, never()).create(any(User.class)); - verify(permissionAssigner, never()).setPermissionsForUser(anyString(), any(Collection.class)); - } - - @Test - void shouldCreateAnonymousUserIfRequired() { - List users = Lists.newArrayList(UserTestData.createTrillian()); - when(userManager.getAll()).thenReturn(users); - when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup()); - when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL); - - setupContextListener.contextInitialized(null); - - verify(userManager).create(SCMContext.ANONYMOUS); - } - - @Test - void shouldNotCreateAnonymousUserIfNotRequired() { - List users = Lists.newArrayList(UserTestData.createTrillian()); - when(userManager.getAll()).thenReturn(users); - when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup()); - - setupContextListener.contextInitialized(null); - - verify(userManager, never()).create(SCMContext.ANONYMOUS); - } - - @Test - void shouldNotCreateAnonymousUserIfAlreadyExists() { - List users = Lists.newArrayList(SCMContext.ANONYMOUS); - when(userManager.getAll()).thenReturn(users); - when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup()); - when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL); - - setupContextListener.contextInitialized(null); - - verify(userManager, times(1)).create(SCMContext.ANONYMOUS); - } - - @Test - void shouldCreateAuthenticatedGroupIfMissing() { - when(groupManager.get(AUTHENTICATED)).thenReturn(null); - - setupContextListener.contextInitialized(null); - - Group authenticated = createAuthenticatedGroup(); - authenticated.setDescription(AUTHENTICATED_GROUP_DESCRIPTION); - authenticated.setExternal(true); - - verify(groupManager, times(1)).create(authenticated); - } - - @Test - void shouldNotCreateAuthenticatedGroupIfAlreadyExists() { - when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup()); - - setupContextListener.contextInitialized(null); - - verify(groupManager, never()).create(any()); - } - - @Test - void shouldCleanupUnfinishedRepositoryExports() { - setupContextListener.contextInitialized(null); - - verify(exportService).cleanupUnfinishedExports(); - } - - private void verifyAdminPermissionsAssigned() { - ArgumentCaptor usernameCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> permissionCaptor = ArgumentCaptor.forClass(Collection.class); - verify(permissionAssigner).setPermissionsForUser(usernameCaptor.capture(), permissionCaptor.capture()); - String username = usernameCaptor.getValue(); - assertThat(username).isEqualTo("scmadmin"); - PermissionDescriptor descriptor = permissionCaptor.getValue().iterator().next(); - assertThat(descriptor.getValue()).isEqualTo("*"); - } - - private void verifyAdminCreated() { - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); - verify(userManager).create(userCaptor.capture()); - User user = userCaptor.getValue(); - assertThat(user.getName()).isEqualTo("scmadmin"); - assertThat(user.getPassword()).isEqualTo("secret"); - } - - private Group createAuthenticatedGroup() { - return new Group("xml", AUTHENTICATED); + verify(administrationContext, times(2)).runAsAdmin(any(PrivilegedStartupAction.class)); } }